#[path = "web_fonts.rs"]
pub mod web_fonts;
use blitz_traits::node_id::NodeId;
use selectors::context::QuirksMode;
use std::sync::atomic::Ordering as Ao;
use std::{
io::Cursor,
sync::{
Arc,
atomic::{AtomicBool, AtomicUsize},
mpsc::Sender,
},
};
use style::{
font_face::{FontFaceSourceFormat, FontFaceSourceFormatKeyword, FontStyleRange, 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, RequestDestination};
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
}
pub(crate) fn with_referrer(mut req: Request, document: &Url) -> Request {
if !matches!(document.scheme(), "http" | "https") {
return req;
}
if document.scheme() == "https" && req.url.scheme() == "http" {
return req;
}
let referrer = if document.origin() == req.url.origin() {
let mut full = document.clone();
full.set_fragment(None);
let _ = full.set_username("");
let _ = full.set_password(None);
full.to_string()
} else {
format!("{}/", document.origin().ascii_serialization())
};
if let Ok(value) = blitz_traits::net::http::HeaderValue::from_str(&referrer) {
req.headers
.insert(blitz_traits::net::http::header::REFERER, value);
}
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, crate::node::SvgImageData),
Css(DocumentStyleSheet),
Font(Bytes, FontFaceOverrides),
TrackedFont(Bytes, FontFaceOverrides, Arc<web_fonts::WebFont>),
DocumentSrc(String),
ImportSheet(ServoArc<Locked<ImportRule>>, ServoArc<Stylesheet>),
None,
}
pub(crate) struct ResourceHandler<T: Send + Sync + 'static> {
doc_id: usize,
request_id: usize,
node_id: Option<NodeId>,
tx: Sender<DocumentEvent>,
shell_provider: Arc<dyn ShellProvider>,
settled: AtomicBool,
data: T,
}
impl<T: Send + Sync + 'static> ResourceHandler<T> {
pub(crate) fn new(
tx: Sender<DocumentEvent>,
doc_id: usize,
node_id: Option<NodeId>,
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,
settled: AtomicBool::new(false),
data,
}
}
pub(crate) fn boxed(
tx: Sender<DocumentEvent>,
doc_id: usize,
node_id: Option<NodeId>,
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>) {
self.settled.store(true, Ao::Release);
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();
}
}
impl<T: Send + Sync + 'static> Drop for ResourceHandler<T> {
fn drop(&mut self) {
if self.settled.load(Ao::Acquire) {
return;
}
let _ = self
.tx
.send(DocumentEvent::ResourceLoad(ResourceLoadResponse {
request_id: self.request_id,
node_id: self.node_id,
resolved_url: None,
result: Err(String::from("resource fetch failed")),
}));
self.shell_provider.request_redraw();
}
}
#[allow(unused)]
pub struct ResourceLoadResponse {
pub request_id: usize,
pub node_id: Option<NodeId>,
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())
.destination(RequestDestination::Style),
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(),
},
),
);
import
}
}
struct NestedStylesheetHandler {
loader: StylesheetLoader,
lock: SharedRwLock,
url: ServoArc<Url>,
media: ServoArc<Locked<MediaList>>,
import_rule: ServoArc<Locked<ImportRule>>,
}
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,
));
let import_rule = self.data.import_rule.clone();
self.respond(resolved_url, Ok(Resource::ImportSheet(import_rule, sheet)))
}
}
struct FontFaceHandler {
format: FontFaceSourceFormatKeyword,
overrides: FontFaceOverrides,
ticket: Option<Arc<web_fonts::WebFont>>,
queued: bool,
}
impl Drop for FontFaceHandler {
fn drop(&mut self) {
if !self.queued {
if let Some(ticket) = &self.ticket {
ticket.finish(false);
}
}
}
}
impl NetHandler for ResourceHandler<FontFaceHandler> {
fn bytes(mut self: Box<Self>, resolved_url: String, bytes: Bytes) {
let result = self.data.parse(bytes);
let result = match (result, self.data.ticket.as_ref()) {
(Ok(Resource::Font(bytes, overrides)), Some(ticket)) => {
self.data.queued = true;
Ok(Resource::TrackedFont(bytes, overrides, Arc::clone(ticket)))
}
(Ok(Resource::None), Some(_)) => Err(String::from("Unsupported font data")),
(result, _) => result,
};
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<NodeId>,
sheet: &Stylesheet,
network_provider: &Arc<dyn NetProvider>,
shell_provider: &Arc<dyn ShellProvider>,
read_guard: &SharedRwLockReadGuard,
abort_signal: Option<&AbortSignal>,
fonts: &mut Vec<Arc<web_fonts::WebFont>>,
epoch: usize,
) {
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()
.and_then(|range| range.0.compute().map(|w| w.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 Some(url) = url_source.url.url() else {
#[cfg(feature = "tracing")]
tracing::warn!("Skipping @font-face source with unresolvable url");
return None;
};
let url = url.as_ref().clone();
Some((url, format))
});
if let Some((url, format)) = preferred_source {
let ticket = web_fonts::WebFont::new(
overrides.clone(),
Some(url.clone()),
None,
true,
epoch,
shell_provider.clone(),
);
fonts.push(Arc::clone(&ticket));
network_provider.fetch(
doc_id,
stamped_request(url, abort_signal).destination(RequestDestination::Font),
ResourceHandler::boxed(
tx.clone(),
doc_id,
node_id,
shell_provider.clone(),
FontFaceHandler {
format,
overrides,
ticket: Some(ticket),
queued: false,
},
),
);
}
})
}
fn load_script_font(doc: &mut crate::BaseDocument, face: &Arc<web_fonts::WebFont>) {
if !face.start() {
return;
}
let mut handler = FontFaceHandler {
format: FontFaceSourceFormatKeyword::None,
overrides: face.overrides.clone(),
ticket: Some(Arc::clone(face)),
queued: false,
};
if let Some(bytes) = &face.data {
match handler.parse(bytes.clone()) {
Ok(Resource::Font(bytes, overrides)) => {
handler.queued = true;
doc.load_resource(ResourceLoadResponse {
request_id: usize::MAX,
node_id: None,
resolved_url: None,
result: Ok(Resource::TrackedFont(bytes, overrides, Arc::clone(face))),
});
}
_ => face.finish(false),
}
} else if let Some(url) = &face.source {
doc.net_provider.fetch(
crate::Document::id(doc),
stamped_request(url.clone(), doc.abort_signal.as_ref())
.destination(RequestDestination::Font),
ResourceHandler::boxed(
doc.tx.clone(),
crate::Document::id(doc),
None,
doc.shell_provider.clone(),
handler,
),
);
} else {
face.finish(false);
}
}
fn stylo_to_fontique_style(style: &FontStyleRange) -> parley::fontique::FontStyle {
use parley::fontique::FontStyle as Fq;
match style {
FontStyleRange::Italic => Fq::Italic,
FontStyleRange::Oblique(min, max) => {
let angle = min.degrees();
if angle.is_none_or(|a| a == 0.0) && max.degrees().is_none_or(|a| a == 0.0) {
Fq::Normal
} else {
Fq::Oblique(angle)
}
}
}
}
pub(crate) struct DocumentSrcHandler;
impl NetHandler for ResourceHandler<DocumentSrcHandler> {
fn bytes(self: Box<Self>, resolved_url: String, bytes: Bytes) {
let html = String::from_utf8_lossy(&bytes).into_owned();
self.respond(resolved_url, Ok(Resource::DocumentSrc(html)));
}
}
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)
}
}
#[cfg(feature = "image-formats")]
pub fn is_jxl(bytes: &[u8]) -> bool {
const CONTAINER: [u8; 12] = [
0, 0, 0, 0x0C, b'J', b'X', b'L', b' ', 0x0D, 0x0A, 0x87, 0x0A,
];
bytes.starts_with(&[0xFF, 0x0A]) || bytes.starts_with(&CONTAINER)
}
#[cfg(feature = "image-formats")]
pub fn decode_jxl(bytes: &[u8]) -> Result<(u32, u32, Vec<u8>), String> {
use jxl_oxide::{EnumColourEncoding, JxlImage, NullCms, RenderingIntent};
const ALLOCATION_LIMIT: u64 = 64 * 1024 * 1024;
let mut image = JxlImage::builder()
.read_bytes(Arc::from(bytes))
.map_err(|error| error.to_string())?;
let (width, height) = (image.width(), image.height());
if u64::from(width) * u64::from(height) * 4 > ALLOCATION_LIMIT {
return Err(format!("{width}x{height} exceeds the decode budget"));
}
image.set_cms(NullCms);
image.request_color_encoding(EnumColourEncoding::srgb(RenderingIntent::Relative));
let render = image.render_frame(0).map_err(|error| error.to_string())?;
let mut stream = render.stream();
let (width, height, channels) = (stream.width(), stream.height(), stream.channels() as usize);
let mut samples = vec![0f32; width as usize * height as usize * channels];
stream.write_to_buffer(&mut samples);
let to_byte = |value: f32| (value.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
let mut rgba = Vec::with_capacity(width as usize * height as usize * 4);
for pixel in samples.chunks_exact(channels) {
let (rgb, alpha) = match channels {
1 => ([pixel[0]; 3], 1.0),
2 => ([pixel[0]; 3], pixel[1]),
3 => ([pixel[0], pixel[1], pixel[2]], 1.0),
_ => ([pixel[0], pixel[1], pixel[2]], pixel[3]),
};
rgba.extend_from_slice(&[
to_byte(rgb[0]),
to_byte(rgb[1]),
to_byte(rgb[2]),
to_byte(alpha),
]);
}
Ok((width, height, rgba))
}
impl ImageHandler {
fn parse(&self, bytes: Bytes) -> Result<Resource, String> {
#[cfg(feature = "image-formats")]
if is_jxl(&bytes) {
return decode_jxl(&bytes)
.map(|(width, height, rgba)| {
Resource::Image(self.kind, width, height, Arc::new(rgba))
})
.map_err(|error| {
format!(
"Could not parse JPEG XL image ({} bytes): {error}",
bytes.len()
)
});
}
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_image;
match parse_svg_image(&bytes) {
Ok(svg) => return Ok(Resource::Svg(self.kind, svg)),
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) -> FontStyleRange {
FontStyleRange::Oblique(Angle::from_degrees(min_deg), Angle::from_degrees(max_deg))
}
#[test]
fn italic_maps_to_italic() {
assert_eq!(stylo_to_fontique_style(&FontStyleRange::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)),
);
}
}