Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion moli-canvas/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ edition = "2024"

[dependencies]
base64 = "0.22"
font8x8 = "0.3"
moli-image = { path = "../moli-image" }
moli-web-mime = { path = "../moli-web-mime" }

Expand Down
22 changes: 0 additions & 22 deletions moli-canvas/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ mod blit;
mod encode;
mod pixel;
mod rect;
mod text;
mod types;

pub use blit::{blit_draw_image, blit_draw_image_filtered, blit_image_data, extract_image_data};
Expand All @@ -15,7 +14,6 @@ pub use pixel::{
scale_rgba8, scale_rgba8_bilinear, scale_rgba8_nearest,
};
pub use rect::{canonicalize_fill_style, fill_style_rgba, normalize_rect, paint_rect};
pub use text::{draw_text, measure_text_width};
pub use types::{
CanvasRect, DEFAULT_FILL_STYLE, DEFAULT_FONT, DrawImageBlit, MAX_RGBA8_BYTE_LENGTH, Rgba8Rect,
ScaleFilter, byte_len,
Expand Down Expand Up @@ -351,26 +349,6 @@ mod tests {
assert_eq!(hot, 4);
}

#[test]
fn measure_text_scales_with_font_size_and_draw_text_changes_pixels() {
let small = measure_text_width("Hi", "10px sans-serif");
let large = measure_text_width("Hi", "24px sans-serif");
assert!(large > small);

let mut pixels = vec![0; byte_len(96, 48).expect("buffer len")];
draw_text(
&mut pixels,
96,
48,
"Moli",
4.0,
24.0,
"16px sans-serif",
fill_style_rgba("#ff0000"),
);
assert!(pixels.iter().any(|&value| value != 0));
}

#[test]
fn draw_image_blit_validation_and_normalized_rect_cover_invalid_inputs() {
assert!(
Expand Down
89 changes: 0 additions & 89 deletions moli-canvas/src/text.rs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
<html>
<body data-order="" data-count="" data-ready-status="">
<script>
// Real project-generated font: moli-layout/tests/fixtures/moli-ahem.woff2.
const FONT_FIXTURE_SOURCE = "url(data:font/woff2;base64,d09GMgABAAAAAAKMAAoAAAAAEewAAAI9AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAABmAARAqSGIlCATYCJAOBRguBRgAEIAWDDweHZhsaDBEVjE2QrxO4OaB11Uo4KkNFQmQIgNrHOq+V6mgnvY2fvSQ/uF+9N7PdNFuXpclV2mkpdFaAC3aRoEdFB3L8iVa3NRGchB7CA+kvmysBs3ExDyF+2La9iSrRM8x/YMEH4+YNnI6ADDhw44HnsPGNf2vg8Vh2oEcLbcza3qYp3gsXSOBToI2cQC+UhHthpAFIuk0EVfwGmbm3vt4jtOf5c3NBuwr0P4kB5kvBCdDgYzQftZAtCfcwRbHKu3S8ycdHAOfu8OUAmLjP+bEQEov+i78AFoAP+AEAxAAAAbQvPfKjw6g3kr+bmM88bAz4CAIItbXF3xu9/n2qfwCnW7usCnIKG0p4i7hd9+SA9QzCt8+eB2LUsMiFeojjkCtwessPXvTy47FKi+3KLnHVsltY375HzLDkOf6NhCkpi4iVsmpyVHYpuS67Fdw/4GE+R/a2N2xwfWXnu79JZ8nsauts62iP3v/+6UJH9FXrZ8WIPds2mEHrVuyYtGLNiW0L8jSL6zWd2nTMNs9acejIxsou80QHe+KtWi8h8LdYBGgJlS666aGXPvoZYJAhhhlhlDHGmbAhTCjjQiptrAMfhFGcpFlelFXdtF0/jNO8rNt+nNf9vB8AQjCCYjhBUjTDcrwgSrKiarphWrbjcb2+/jc6gLBCmFDGhVTaWBf+NANEmFDGhVTaWBf+tABEmFDGhVTaWBf+tAJEmFDGhVTaWBf+tAFEmFDGhVTaWBf+tANEmFDGhVTa2PClCtb3AQAAAA==)";
document.fonts.add(new FontFace('FixtureFace', FONT_FIXTURE_SOURCE));
const body = document.body;
const events = [];

Expand All @@ -10,7 +13,7 @@
document.fonts.onloadingerror = (event) => events.push(event.type);

document.fonts
.load('10px sans-serif')
.load('10px FixtureFace')
.then(() => document.fonts.ready)
.then(() => {
body.setAttribute('data-order', events.join(','));
Expand Down
13 changes: 8 additions & 5 deletions moli-core/tests/fixtures/runtime/document_fonts_events.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
<html>
<body data-loading-a="" data-loading-b="" data-loadingdone="">
<script>
// Real project-generated font: moli-layout/tests/fixtures/moli-ahem.woff2.
const FONT_FIXTURE_SOURCE = "url(data:font/woff2;base64,d09GMgABAAAAAAKMAAoAAAAAEewAAAI9AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAABmAARAqSGIlCATYCJAOBRguBRgAEIAWDDweHZhsaDBEVjE2QrxO4OaB11Uo4KkNFQmQIgNrHOq+V6mgnvY2fvSQ/uF+9N7PdNFuXpclV2mkpdFaAC3aRoEdFB3L8iVa3NRGchB7CA+kvmysBs3ExDyF+2La9iSrRM8x/YMEH4+YNnI6ADDhw44HnsPGNf2vg8Vh2oEcLbcza3qYp3gsXSOBToI2cQC+UhHthpAFIuk0EVfwGmbm3vt4jtOf5c3NBuwr0P4kB5kvBCdDgYzQftZAtCfcwRbHKu3S8ycdHAOfu8OUAmLjP+bEQEov+i78AFoAP+AEAxAAAAbQvPfKjw6g3kr+bmM88bAz4CAIItbXF3xu9/n2qfwCnW7usCnIKG0p4i7hd9+SA9QzCt8+eB2LUsMiFeojjkCtwessPXvTy47FKi+3KLnHVsltY375HzLDkOf6NhCkpi4iVsmpyVHYpuS67Fdw/4GE+R/a2N2xwfWXnu79JZ8nsauts62iP3v/+6UJH9FXrZ8WIPds2mEHrVuyYtGLNiW0L8jSL6zWd2nTMNs9acejIxsou80QHe+KtWi8h8LdYBGgJlS666aGXPvoZYJAhhhlhlDHGmbAhTCjjQiptrAMfhFGcpFlelFXdtF0/jNO8rNt+nNf9vB8AQjCCYjhBUjTDcrwgSrKiarphWrbjcb2+/jc6gLBCmFDGhVTaWBf+NANEmFDGhVTaWBf+tABEmFDGhVTaWBf+tAJEmFDGhVTaWBf+tAFEmFDGhVTaWBf+tANEmFDGhVTa2PClCtb3AQAAAA==)";
document.fonts.add(new FontFace('Roboto', FONT_FIXTURE_SOURCE));
let loadingA = 0;
let loadingB = 0;
let loadingDone = 0;
Expand All @@ -22,11 +25,11 @@
document.fonts.addEventListener('loading', onLoadingB);
document.fonts.addEventListener('loadingdone', onLoadingDone);
document.fonts.removeEventListener('loading', onLoadingA);
document.fonts.load('italic bold 16px Roboto');

document.body.setAttribute('data-loading-a', String(loadingA));
document.body.setAttribute('data-loading-b', String(loadingB));
document.body.setAttribute('data-loadingdone', String(loadingDone));
document.fonts.load('italic bold 16px Roboto').then(() => document.fonts.ready).then(() => {
document.body.setAttribute('data-loading-a', String(loadingA));
document.body.setAttribute('data-loading-b', String(loadingB));
document.body.setAttribute('data-loadingdone', String(loadingDone));
});
</script>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
data-simple-error=""
>
<script>
// Real project-generated font: moli-layout/tests/fixtures/moli-ahem.woff2.
const FONT_FIXTURE_SOURCE = "url(data:font/woff2;base64,d09GMgABAAAAAAKMAAoAAAAAEewAAAI9AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAABmAARAqSGIlCATYCJAOBRguBRgAEIAWDDweHZhsaDBEVjE2QrxO4OaB11Uo4KkNFQmQIgNrHOq+V6mgnvY2fvSQ/uF+9N7PdNFuXpclV2mkpdFaAC3aRoEdFB3L8iVa3NRGchB7CA+kvmysBs3ExDyF+2La9iSrRM8x/YMEH4+YNnI6ADDhw44HnsPGNf2vg8Vh2oEcLbcza3qYp3gsXSOBToI2cQC+UhHthpAFIuk0EVfwGmbm3vt4jtOf5c3NBuwr0P4kB5kvBCdDgYzQftZAtCfcwRbHKu3S8ycdHAOfu8OUAmLjP+bEQEov+i78AFoAP+AEAxAAAAbQvPfKjw6g3kr+bmM88bAz4CAIItbXF3xu9/n2qfwCnW7usCnIKG0p4i7hd9+SA9QzCt8+eB2LUsMiFeojjkCtwessPXvTy47FKi+3KLnHVsltY375HzLDkOf6NhCkpi4iVsmpyVHYpuS67Fdw/4GE+R/a2N2xwfWXnu79JZ8nsauts62iP3v/+6UJH9FXrZ8WIPds2mEHrVuyYtGLNiW0L8jSL6zWd2nTMNs9acejIxsou80QHe+KtWi8h8LdYBGgJlS666aGXPvoZYJAhhhlhlDHGmbAhTCjjQiptrAMfhFGcpFlelFXdtF0/jNO8rNt+nNf9vB8AQjCCYjhBUjTDcrwgSrKiarphWrbjcb2+/jc6gLBCmFDGhVTaWBf+NANEmFDGhVTaWBf+tABEmFDGhVTaWBf+tAJEmFDGhVTaWBf+tAFEmFDGhVTaWBf+tANEmFDGhVTa2PClCtb3AQAAAA==)";
document.fonts.add(new FontFace('Roboto', FONT_FIXTURE_SOURCE));
const body = document.body;

const fontOrder = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
data-css-wide-keyword-prefixed=""
>
<script>
// Real project-generated font: moli-layout/tests/fixtures/moli-ahem.woff2.
const FONT_FIXTURE_SOURCE = "url(data:font/woff2;base64,d09GMgABAAAAAAKMAAoAAAAAEewAAAI9AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAABmAARAqSGIlCATYCJAOBRguBRgAEIAWDDweHZhsaDBEVjE2QrxO4OaB11Uo4KkNFQmQIgNrHOq+V6mgnvY2fvSQ/uF+9N7PdNFuXpclV2mkpdFaAC3aRoEdFB3L8iVa3NRGchB7CA+kvmysBs3ExDyF+2La9iSrRM8x/YMEH4+YNnI6ADDhw44HnsPGNf2vg8Vh2oEcLbcza3qYp3gsXSOBToI2cQC+UhHthpAFIuk0EVfwGmbm3vt4jtOf5c3NBuwr0P4kB5kvBCdDgYzQftZAtCfcwRbHKu3S8ycdHAOfu8OUAmLjP+bEQEov+i78AFoAP+AEAxAAAAbQvPfKjw6g3kr+bmM88bAz4CAIItbXF3xu9/n2qfwCnW7usCnIKG0p4i7hd9+SA9QzCt8+eB2LUsMiFeojjkCtwessPXvTy47FKi+3KLnHVsltY375HzLDkOf6NhCkpi4iVsmpyVHYpuS67Fdw/4GE+R/a2N2xwfWXnu79JZ8nsauts62iP3v/+6UJH9FXrZ8WIPds2mEHrVuyYtGLNiW0L8jSL6zWd2nTMNs9acejIxsou80QHe+KtWi8h8LdYBGgJlS666aGXPvoZYJAhhhlhlDHGmbAhTCjjQiptrAMfhFGcpFlelFXdtF0/jNO8rNt+nNf9vB8AQjCCYjhBUjTDcrwgSrKiarphWrbjcb2+/jc6gLBCmFDGhVTaWBf+NANEmFDGhVTaWBf+tABEmFDGhVTaWBf+tAJEmFDGhVTaWBf+tAFEmFDGhVTaWBf+tANEmFDGhVTa2PClCtb3AQAAAA==)";
document.fonts.add(new FontFace('FixtureFace', FONT_FIXTURE_SOURCE));
(async () => {
const body = document.body;
const ready1 = document.fonts.ready;
Expand Down
149 changes: 149 additions & 0 deletions moli-css-parse/src/font_face.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,121 @@ use crate::unquote_css_string;

pub use style::moli_font_face::{CssFontFace, normalize_font_face_src, parse_font_faces};

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CssFontSource {
Local(String),
Url(String),
}

/// Parse through Stylo first; the token walk only extracts validated sources.
pub fn parse_font_face_sources(source: &str) -> Option<Vec<CssFontSource>> {
let normalized = normalize_font_face_src(source)?;
let mut input = ParserInput::new(&normalized);
let mut parser = Parser::new(&mut input);
parser
.parse_comma_separated(|parser| {
let name = parser.expect_function()?.clone();
let source = parser.parse_nested_block(|nested| {
if name.eq_ignore_ascii_case("local") {
let mut parts = Vec::new();
while !nested.is_exhausted() {
parts.push(nested.expect_ident_or_string()?.to_string());
}
Ok(CssFontSource::Local(parts.join(" ")))
} else if name.eq_ignore_ascii_case("url") {
Ok(CssFontSource::Url(nested.expect_string()?.to_string()))
} else {
Err(nested.new_custom_error(()))
}
})?;
let mut supported = true;
while !parser.is_exhausted() {
let kind = parser.expect_function()?.clone();
parser.parse_nested_block(|nested| {
let mut values = Vec::new();
while !nested.is_exhausted() {
if nested.try_parse(|p| p.expect_comma()).is_ok() {
continue;
}
values.push(nested.expect_ident_or_string()?.to_ascii_lowercase());
}
supported &= kind.eq_ignore_ascii_case("format")
&& values.iter().all(|v| {
matches!(
v.as_str(),
"woff" | "woff2" | "truetype" | "opentype" | "collection"
)
});
Ok::<_, cssparser::ParseError<'_, ()>>(())
})?;
}
Ok(supported.then_some(source))
})
.ok()
.map(|sources| sources.into_iter().flatten().collect())
}

#[derive(Clone, Debug)]
pub struct CssFontShorthand {
pub family_css: String,
pub families: Vec<String>,
pub size: String,
pub style: String,
pub weight: String,
pub stretch: String,
}

/// Shares the CSS font grammar with style declarations, including quoted
/// multi-word families and fallback lists. No guessed last-whitespace token.
pub fn parse_font_shorthand(value: &str) -> Option<CssFontShorthand> {
use style::{
properties::{PropertyId, parse_property_declaration_list},
stylesheets::CssRuleType,
};
use style_traits::CssStringWriter;
if crate::escape_top_level_semicolons(value) != value
|| crate::split_important_priority(value).1
{
return None;
}
crate::stylo_stylesheet::with_descriptor_declaration_context(CssRuleType::Style, |context| {
let text = format!("font: {value}");
let mut input = ParserInput::new(&text);
let block = parse_property_declaration_list(context, &mut Parser::new(&mut input), &[]);
let property = |name: &str| -> Option<String> {
let mut out = CssStringWriter::new();
block
.property_value_to_css(&PropertyId::parse(name, context).ok()?, &mut out)
.ok()?;
(!out.is_empty()).then(|| out.to_string())
};
let family_css = property("font-family")?;
let size = property("font-size")?;
if is_css_wide_keyword(&size) || size.contains("var(") {
return None;
}
let mut input = ParserInput::new(&family_css);
let families = Parser::new(&mut input)
.parse_comma_separated(|p| {
let mut parts = Vec::new();
while !p.is_exhausted() {
parts.push(p.expect_ident_or_string()?.to_string());
}
Ok::<_, cssparser::ParseError<'_, ()>>(parts.join(" "))
})
.ok()?;
drop(input);
Some(CssFontShorthand {
family_css,
families,
size,
style: property("font-style")?,
weight: property("font-weight")?,
stretch: property("font-stretch")?,
})
})
}

pub fn font_load_query_contains_css_wide_keyword(query: &str) -> bool {
let mut input = ParserInput::new(query);
let mut input = Parser::new(&mut input);
Expand Down Expand Up @@ -50,6 +165,40 @@ fn is_css_wide_keyword(value: &str) -> bool {

#[cfg(test)]
mod tests {
#[test]
fn font_sources_preserve_local_names_and_supported_url_fallback_order() {
use super::*;
assert_eq!(
parse_font_face_sources(
"local(Missing Font), url(bad) format('unsupported'), url(good.woff2) format('woff2')"
),
Some(vec![
CssFontSource::Local("Missing Font".into()),
CssFontSource::Url("good.woff2".into())
])
);
assert!(parse_font_face_sources("not a source").is_none());
}

#[test]
fn font_shorthand_uses_stylo_validation_and_keeps_all_families() {
let font =
super::parse_font_shorthand("italic bold 10.5px/1.2 \"A B\", Another Face, serif")
.unwrap();
assert_eq!(font.families, ["A B", "Another Face", "serif"]);
assert_eq!(font.size, "10.5px");
assert_eq!(font.style, "italic");
for invalid in [
"",
"12px",
"not a font",
"inherit",
"12px serif; color:red",
"12px serif !important",
] {
assert!(super::parse_font_shorthand(invalid).is_none(), "{invalid}");
}
}
use super::{
font_load_query_contains_css_wide_keyword, font_load_query_family, normalize_font_face_src,
parse_font_faces,
Expand Down
Loading
Loading