use selectors::context::QuirksMode;
use std::sync::atomic::Ordering as Ao;
use std::{
io::Cursor,
sync::{Arc, atomic::AtomicUsize, mpsc::Sender},
};
use style::{
font_face::{
FontFaceSourceFormat, FontFaceSourceFormatKeyword, FontStyle as StyloFontStyle, Source,
},
media_queries::MediaList,
servo_arc::Arc as ServoArc,
shared_lock::SharedRwLock,
shared_lock::{Locked, SharedRwLockReadGuard},
stylesheets::{
AllowImportRules, CssRule, DocumentStyleSheet, ImportRule, Origin, Stylesheet,
StylesheetInDocument, StylesheetLoader as ServoStylesheetLoader, UrlExtraData,
import_rule::{ImportLayer, ImportSheet, ImportSupportsCondition},
},
values::{CssUrl, SourceLocation},
};
use blitz_traits::net::{AbortSignal, Bytes, NetHandler, NetProvider, Request};
use blitz_traits::shell::ShellProvider;
use url::Url;
use crate::{document::DocumentEvent, util::ImageType};
pub(crate) fn stamped_request(url: Url, signal: Option<&AbortSignal>) -> Request {
let mut req = Request::get(url);
if let Some(sig) = signal {
req = req.signal(sig.clone());
}
req
}
#[derive(Clone, Debug, Default)]
pub struct FontFaceOverrides {
pub family_name: Option<String>,
pub weight: Option<f32>,
pub style: Option<parley::fontique::FontStyle>,
}
#[derive(Clone, Debug)]
pub enum Resource {
Image(ImageType, u32, u32, Arc<Vec<u8>>),
#[cfg(feature = "svg")]
Svg(ImageType, Arc<usvg::Tree>),
Css(DocumentStyleSheet),
Font(Bytes, FontFaceOverrides),
None,
}
pub(crate) struct ResourceHandler<T: Send + Sync + 'static> {
doc_id: usize,
request_id: usize,
node_id: Option<usize>,
tx: Sender<DocumentEvent>,
shell_provider: Arc<dyn ShellProvider>,
data: T,
}
impl<T: Send + Sync + 'static> ResourceHandler<T> {
pub(crate) fn new(
tx: Sender<DocumentEvent>,
doc_id: usize,
node_id: Option<usize>,
shell_provider: Arc<dyn ShellProvider>,
data: T,
) -> Self {
static REQUEST_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
Self {
request_id: REQUEST_ID_COUNTER.fetch_add(1, Ao::Relaxed),
doc_id,
node_id,
tx,
shell_provider,
data,
}
}
pub(crate) fn boxed(
tx: Sender<DocumentEvent>,
doc_id: usize,
node_id: Option<usize>,
shell_provider: Arc<dyn ShellProvider>,
data: T,
) -> Box<dyn NetHandler>
where
ResourceHandler<T>: NetHandler,
{
Box::new(Self::new(tx, doc_id, node_id, shell_provider, data)) as _
}
pub(crate) fn request_id(&self) -> usize {
self.request_id
}
fn respond(&self, resolved_url: String, result: Result<Resource, String>) {
let response = ResourceLoadResponse {
request_id: self.request_id,
node_id: self.node_id,
resolved_url: Some(resolved_url),
result,
};
let _ = self.tx.send(DocumentEvent::ResourceLoad(response));
self.shell_provider.request_redraw();
}
}
#[allow(unused)]
pub struct ResourceLoadResponse {
pub request_id: usize,
pub node_id: Option<usize>,
pub resolved_url: Option<String>,
pub result: Result<Resource, String>,
}
pub struct StylesheetHandler {
pub source_url: Url,
pub guard: SharedRwLock,
pub net_provider: Arc<dyn NetProvider>,
pub abort_signal: Option<AbortSignal>,
}
impl NetHandler for ResourceHandler<StylesheetHandler> {
fn bytes(self: Box<Self>, resolved_url: String, bytes: Bytes) {
let Ok(css) = std::str::from_utf8(&bytes) else {
return self.respond(resolved_url, Err(String::from("Invalid UTF8")));
};
let sheet = Stylesheet::from_str(
css,
self.data.source_url.clone().into(),
Origin::Author,
ServoArc::new(self.data.guard.wrap(MediaList::empty())),
self.data.guard.clone(),
Some(&StylesheetLoader {
tx: self.tx.clone(),
doc_id: self.doc_id,
net_provider: self.data.net_provider.clone(),
shell_provider: self.shell_provider.clone(),
abort_signal: self.data.abort_signal.clone(),
}),
None, QuirksMode::NoQuirks,
AllowImportRules::Yes,
);
self.respond(
resolved_url,
Ok(Resource::Css(DocumentStyleSheet(ServoArc::new(sheet)))),
);
}
}
#[derive(Clone)]
pub(crate) struct StylesheetLoader {
pub(crate) tx: Sender<DocumentEvent>,
pub(crate) doc_id: usize,
pub(crate) net_provider: Arc<dyn NetProvider>,
pub(crate) shell_provider: Arc<dyn ShellProvider>,
pub(crate) abort_signal: Option<AbortSignal>,
}
impl ServoStylesheetLoader for StylesheetLoader {
fn request_stylesheet(
&self,
url: CssUrl,
location: SourceLocation,
lock: &SharedRwLock,
media: ServoArc<Locked<MediaList>>,
supports: Option<ImportSupportsCondition>,
layer: ImportLayer,
) -> ServoArc<Locked<ImportRule>> {
if !supports.as_ref().is_none_or(|s| s.enabled) {
return ServoArc::new(lock.wrap(ImportRule {
url,
stylesheet: ImportSheet::new_refused(),
supports,
layer,
source_location: location,
}));
}
let import = ImportRule {
url,
stylesheet: ImportSheet::new_pending(),
supports,
layer,
source_location: location,
};
let url = import.url.url().unwrap().clone();
let import = ServoArc::new(lock.wrap(import));
self.net_provider.fetch(
self.doc_id,
stamped_request(url.as_ref().clone(), self.abort_signal.as_ref()),
ResourceHandler::boxed(
self.tx.clone(),
self.doc_id,
None, self.shell_provider.clone(),
NestedStylesheetHandler {
url: url.clone(),
loader: self.clone(),
lock: lock.clone(),
media,
import_rule: import.clone(),
net_provider: self.net_provider.clone(),
},
),
);
import
}
}
struct NestedStylesheetHandler {
loader: StylesheetLoader,
lock: SharedRwLock,
url: ServoArc<Url>,
media: ServoArc<Locked<MediaList>>,
import_rule: ServoArc<Locked<ImportRule>>,
net_provider: Arc<dyn NetProvider>,
}
impl NetHandler for ResourceHandler<NestedStylesheetHandler> {
fn bytes(self: Box<Self>, resolved_url: String, bytes: Bytes) {
let Ok(css) = std::str::from_utf8(&bytes) else {
return self.respond(resolved_url, Err(String::from("Invalid UTF8")));
};
let sheet = ServoArc::new(Stylesheet::from_str(
css,
UrlExtraData(self.data.url.clone()),
Origin::Author,
self.data.media.clone(),
self.data.lock.clone(),
Some(&self.data.loader),
None, QuirksMode::NoQuirks,
AllowImportRules::Yes,
));
fetch_font_face(
self.tx.clone(),
self.doc_id,
self.node_id,
&sheet,
&self.data.net_provider,
&self.shell_provider,
&self.data.lock.read(),
self.data.loader.abort_signal.as_ref(),
);
let mut guard = self.data.lock.write();
self.data.import_rule.write_with(&mut guard).stylesheet = ImportSheet::Sheet(sheet);
drop(guard);
self.respond(resolved_url, Ok(Resource::None))
}
}
struct FontFaceHandler {
format: FontFaceSourceFormatKeyword,
overrides: FontFaceOverrides,
}
impl NetHandler for ResourceHandler<FontFaceHandler> {
fn bytes(mut self: Box<Self>, resolved_url: String, bytes: Bytes) {
let result = self.data.parse(bytes);
self.respond(resolved_url, result)
}
}
impl FontFaceHandler {
fn parse(&mut self, bytes: Bytes) -> Result<Resource, String> {
if self.format == FontFaceSourceFormatKeyword::None && bytes.len() >= 4 {
self.format = match &bytes.as_ref()[0..4] {
#[cfg(feature = "woff")]
b"wOFF" => FontFaceSourceFormatKeyword::Woff,
#[cfg(feature = "woff")]
b"wOF2" => FontFaceSourceFormatKeyword::Woff2,
b"OTTO" => FontFaceSourceFormatKeyword::Opentype,
&[0x00, 0x01, 0x00, 0x00] => FontFaceSourceFormatKeyword::Truetype,
b"true" => FontFaceSourceFormatKeyword::Truetype,
_ => FontFaceSourceFormatKeyword::None,
}
}
#[cfg(feature = "woff")]
let mut bytes = bytes;
match self.format {
#[cfg(feature = "woff")]
FontFaceSourceFormatKeyword::Woff => {
#[cfg(feature = "tracing")]
tracing::info!("Decompressing woff1 font");
let decompressed = wuff::decompress_woff1(&bytes).ok();
if let Some(decompressed) = decompressed {
bytes = Bytes::from(decompressed);
} else {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to decompress woff1 font");
}
}
#[cfg(feature = "woff")]
FontFaceSourceFormatKeyword::Woff2 => {
#[cfg(feature = "tracing")]
tracing::info!("Decompressing woff2 font");
let decompressed = wuff::decompress_woff2(&bytes).ok();
if let Some(decompressed) = decompressed {
bytes = Bytes::from(decompressed);
} else {
#[cfg(feature = "tracing")]
tracing::warn!("Failed to decompress woff2 font");
}
}
FontFaceSourceFormatKeyword::None => {
return Ok(Resource::None);
}
_ => {}
}
Ok(Resource::Font(bytes, std::mem::take(&mut self.overrides)))
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn fetch_font_face(
tx: Sender<DocumentEvent>,
doc_id: usize,
node_id: Option<usize>,
sheet: &Stylesheet,
network_provider: &Arc<dyn NetProvider>,
shell_provider: &Arc<dyn ShellProvider>,
read_guard: &SharedRwLockReadGuard,
abort_signal: Option<&AbortSignal>,
) {
sheet
.contents(read_guard)
.rules(read_guard)
.iter()
.filter_map(|rule| match rule {
CssRule::FontFace(font_face) => {
let descriptor = &font_face.read_with(read_guard).descriptors;
let family = descriptor.font_family.as_ref()?;
let src = descriptor.src.as_ref()?;
let overrides = FontFaceOverrides {
family_name: Some(family.name.to_string()),
weight: descriptor
.font_weight
.as_ref()
.map(|range| range.0.compute().value()),
style: descriptor.font_style.as_ref().map(stylo_to_fontique_style),
};
Some((src, overrides))
}
_ => None,
})
.for_each(|(source_list, overrides)| {
let preferred_source = source_list
.0
.iter()
.filter_map(|source| match source {
Source::Url(url_source) => Some(url_source),
Source::Local(_) => None,
})
.find_map(|url_source| {
let mut format = match &url_source.format_hint {
Some(FontFaceSourceFormat::Keyword(fmt)) => *fmt,
Some(FontFaceSourceFormat::String(str)) => match str.as_str() {
"woff2" => FontFaceSourceFormatKeyword::Woff2,
"ttf" => FontFaceSourceFormatKeyword::Truetype,
"otf" => FontFaceSourceFormatKeyword::Opentype,
_ => FontFaceSourceFormatKeyword::None,
},
_ => FontFaceSourceFormatKeyword::None,
};
if format == FontFaceSourceFormatKeyword::None {
let (_, end) = url_source.url.as_str().rsplit_once('.')?;
format = match end {
"woff2" => FontFaceSourceFormatKeyword::Woff2,
"woff" => FontFaceSourceFormatKeyword::Woff,
"ttf" => FontFaceSourceFormatKeyword::Truetype,
"otf" => FontFaceSourceFormatKeyword::Opentype,
"svg" => FontFaceSourceFormatKeyword::Svg,
"eot" => FontFaceSourceFormatKeyword::EmbeddedOpentype,
_ => FontFaceSourceFormatKeyword::None,
}
}
if matches!(
format,
FontFaceSourceFormatKeyword::Svg
| FontFaceSourceFormatKeyword::EmbeddedOpentype
) {
#[cfg(feature = "tracing")]
tracing::warn!("Skipping unsupported font of type {:?}", format);
return None;
}
#[cfg(not(feature = "woff"))]
if matches!(
format,
FontFaceSourceFormatKeyword::Woff | FontFaceSourceFormatKeyword::Woff2
) {
#[cfg(feature = "tracing")]
tracing::warn!("Skipping unsupported font of type {:?}", format);
return None;
}
let url = url_source.url.url().unwrap().as_ref().clone();
Some((url, format))
});
if let Some((url, format)) = preferred_source {
network_provider.fetch(
doc_id,
stamped_request(url, abort_signal),
ResourceHandler::boxed(
tx.clone(),
doc_id,
node_id,
shell_provider.clone(),
FontFaceHandler { format, overrides },
),
);
}
})
}
fn stylo_to_fontique_style(style: &StyloFontStyle) -> parley::fontique::FontStyle {
use parley::fontique::FontStyle as Fq;
match style {
StyloFontStyle::Italic => Fq::Italic,
StyloFontStyle::Oblique(min, max) => {
let angle = min.degrees();
if angle == 0.0 && max.degrees() == 0.0 {
Fq::Normal
} else {
Fq::Oblique(Some(angle))
}
}
}
}
pub struct ImageHandler {
kind: ImageType,
}
impl ImageHandler {
pub fn new(kind: ImageType) -> Self {
Self { kind }
}
}
impl NetHandler for ResourceHandler<ImageHandler> {
fn bytes(self: Box<Self>, resolved_url: String, bytes: Bytes) {
let result = self.data.parse(bytes);
self.respond(resolved_url, result)
}
}
impl ImageHandler {
fn parse(&self, bytes: Bytes) -> Result<Resource, String> {
let image_err = match image::ImageReader::new(Cursor::new(&bytes))
.with_guessed_format()
.expect("IO errors impossible with Cursor")
.decode()
{
Ok(image) => {
let raw_rgba8_data = image.clone().into_rgba8().into_raw();
return Ok(Resource::Image(
self.kind,
image.width(),
image.height(),
Arc::new(raw_rgba8_data),
));
}
Err(e) => e.to_string(),
};
#[cfg(feature = "svg")]
let svg_err = {
use crate::util::parse_svg;
match parse_svg(&bytes) {
Ok(tree) => return Ok(Resource::Svg(self.kind, Arc::new(tree))),
Err(e) => e.to_string(),
}
};
#[cfg(not(feature = "svg"))]
let svg_err = "svg feature disabled";
Err(format!(
"Could not parse image ({} bytes): image-crate error: {image_err}; svg fallback error: {svg_err}",
bytes.len()
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use parley::fontique::FontStyle as Fq;
use style::values::specified::Angle;
fn oblique(min_deg: f32, max_deg: f32) -> StyloFontStyle {
StyloFontStyle::Oblique(
Angle::from_degrees(min_deg, false),
Angle::from_degrees(max_deg, false),
)
}
#[test]
fn italic_maps_to_italic() {
assert_eq!(stylo_to_fontique_style(&StyloFontStyle::Italic), Fq::Italic,);
}
#[test]
fn oblique_zero_zero_maps_to_normal() {
assert_eq!(stylo_to_fontique_style(&oblique(0.0, 0.0)), Fq::Normal);
}
#[test]
fn oblique_single_angle_maps_to_oblique_with_min() {
assert_eq!(
stylo_to_fontique_style(&oblique(14.0, 14.0)),
Fq::Oblique(Some(14.0)),
);
}
#[test]
fn oblique_range_uses_min_angle() {
assert_eq!(
stylo_to_fontique_style(&oblique(10.0, 20.0)),
Fq::Oblique(Some(10.0)),
);
}
}