@@ -0,0 +1,892 @@
1+// Surface/path/paint/readback patterns adapted from fframes at
2+// 4ffb052d3430e0672b4f3647450c181606ddf418. See ../licenses/fframes.txt.
3+use std::collections::HashMap;
7+use fontdb::{Database, Family, Query, Source, Stretch, Style, Weight, ID};
8+use rustybuzz::{Face, Feature, UnicodeBuffer};
9+use skia_safe::canvas::SaveLayerRec;
11+ color_filters, image_filters, paint, surfaces, AlphaType, BlendMode, Canvas, ClipOp, ColorType,
12+ Data, FilterMode, Font, FontHinting, FontMgr, ImageInfo, Matrix, Paint, PaintStyle, Path,
13+ PathBuilder, PathFillType, PictureRecorder, Point, Rect, Surface, TileMode, Typeface,
15+use unicode_script::{Script, UnicodeScript};
16+use unicode_segmentation::UnicodeSegmentation;
18+use crate::{failure, FontAsset, HostResult};
21+pub(crate) struct Metrics {
27+pub(crate) type Color = [u8; 4];
29+pub(crate) struct Stroke {
36+pub(crate) struct CirclePattern {
38+ pub background: Color,
42+ pub origin: [f32; 2],
48+ tint: Option<skia_safe::ColorFilter>,
54+ saved: Vec<DrawStyle>,
57+struct RequestedFont {
70+ positions: Vec<Point>,
73+pub(crate) struct Graphics {
74+ fonts: Arc<Database>,
75+ features: HashMap<ID, Vec<Feature>>,
76+ font_manager: FontMgr,
77+ typefaces: HashMap<ID, Typeface>,
78+ surfaces: HashMap<u32, Raster>,
83+ pub fn new(assets: &[FontAsset]) -> HostResult<Self> {
84+ let mut fonts = Database::new();
85+ let mut features = HashMap::new();
86+ for asset in assets {
87+ let ids = fonts.load_font_source(Source::Binary(Arc::new(asset.data)));
89+ return Err(failure(format!("invalid font asset: {}", asset.family)));
91+ let parsed_features = asset
94+ .filter(|part| !part.trim().is_empty())
96+ Feature::from_str(part.trim())
97+ .map_err(|_| failure(format!("invalid OpenType feature: {part}")))
99+ .collect::<HostResult<Vec<_>>>()?;
101+ let mut face = fonts
103+ .ok_or_else(|| failure("loaded font face is unavailable"))?
105+ fonts.remove_face(id);
106+ // Explicit aliases and metadata are authoritative. The same bytes
107+ // may be registered under several CSS names without copying them.
108+ face.families.insert(
110+ (asset.family.into(), fontdb::Language::English_UnitedStates),
112+ face.weight = Weight(asset.weight);
113+ face.style = if asset.italic {
118+ let id = fonts.push_face_info(face);
119+ features.insert(id, parsed_features.clone());
122+ // Supplied faces precede system faces in otherwise identical CSS matches.
123+ fonts.load_system_fonts();
124+ if fonts.is_empty() {
125+ return Err(failure("no fonts are available"));
128+ fonts: Arc::new(fonts),
130+ font_manager: FontMgr::new(),
131+ typefaces: HashMap::new(),
132+ surfaces: HashMap::new(),
137+ pub fn measure(&self, shorthand: &str, text: &str) -> HostResult<Metrics> {
138+ self.shape_text(shorthand, text, None)
139+ .map(|(metrics, _, _)| metrics)
146+ mut runs: Option<&mut Vec<GlyphRun>>,
147+ ) -> HostResult<(Metrics, ID, f64)> {
148+ let font = svgtypes::FontShorthand::from_str(shorthand)
149+ .map_err(|error| failure(format!("invalid CSS font {shorthand:?}: {error}")))?;
150+ let size = font_size(font.font_size)?;
151+ let families = svgtypes::parse_font_families(font.font_family)?;
152+ let names: Vec<_> = families
154+ .map(|family| match family {
155+ svgtypes::FontFamily::Named(name) => Family::Name(name),
156+ svgtypes::FontFamily::Serif => Family::Serif,
157+ svgtypes::FontFamily::SansSerif => Family::SansSerif,
158+ svgtypes::FontFamily::Monospace => Family::Monospace,
159+ svgtypes::FontFamily::Cursive => Family::Cursive,
160+ svgtypes::FontFamily::Fantasy => Family::Fantasy,
163+ let weight = match font.font_weight.unwrap_or("normal") {
165+ "bold" | "bolder" => 700,
167+ value => value.parse::<u16>()?,
169+ let style = match font.font_style {
170+ Some("italic") => Style::Italic,
171+ Some("oblique") => Style::Oblique,
174+ let stretch = match font.font_stretch {
175+ Some("ultra-condensed") => Stretch::UltraCondensed,
176+ Some("extra-condensed") => Stretch::ExtraCondensed,
177+ Some("condensed") => Stretch::Condensed,
178+ Some("semi-condensed") => Stretch::SemiCondensed,
179+ Some("semi-expanded") => Stretch::SemiExpanded,
180+ Some("expanded") => Stretch::Expanded,
181+ Some("extra-expanded") => Stretch::ExtraExpanded,
182+ Some("ultra-expanded") => Stretch::UltraExpanded,
183+ _ => Stretch::Normal,
189+ weight: Weight(weight),
194+ self.fonts.query(&Query {
195+ families: &[Family::SansSerif],
196+ weight: Weight(weight),
201+ .or_else(|| self.fonts.faces().next().map(|face| face.id))
202+ .ok_or_else(|| failure(format!("no font matches {shorthand:?}")))?;
203+ let requested = RequestedFont {
207+ small_caps: font.font_variant == Some("small-caps"),
209+ let mut metrics = Metrics::default();
210+ let mut run_start = 0;
211+ let mut run_font = base;
212+ let mut run_script = Script::Common;
213+ // Keep combining marks and emoji sequences together during fallback.
214+ // Script changes split shaping runs, but common punctuation and inherited
215+ // marks retain the surrounding script instead of breaking ligatures.
216+ for (offset, grapheme) in text.grapheme_indices(true) {
217+ let selected = self.fallback(base, grapheme);
218+ let script = grapheme
220+ .map(|c| c.script())
221+ .find(|script| !matches!(script, Script::Common | Script::Inherited))
222+ .unwrap_or(run_script);
223+ if offset > run_start
224+ && (selected != run_font || (script != run_script && run_script != Script::Common))
228+ &text[run_start..offset],
231+ runs.as_deref_mut(),
235+ run_font = selected;
236+ run_script = script;
238+ if run_start < text.len() {
244+ runs.as_deref_mut(),
247+ Ok((metrics, base, size))
250+ fn supports(&self, id: ID, text: &str) -> bool {
251+ self.fonts.with_face_data(id, |data, index| {
252+ Face::from_slice(data, index).map(|face| text.chars().all(|c| {
253+ // Joiners and variation selectors modify their surrounding glyphs.
254+ matches!(c, '\u{200c}' | '\u{200d}' | '\u{fe00}'..='\u{fe0f}' | '\u{e0100}'..='\u{e01ef}')
255+ || face.glyph_index(c).is_some()
256+ })).unwrap_or(false)
260+ fn fallback(&self, base: ID, text: &str) -> ID {
261+ if self.supports(base, text) {
264+ let original = self.fonts.face(base);
270+ face.style == original.style
271+ || face.weight == original.weight
272+ || face.stretch == original.stretch
276+ .find(|face| self.supports(face.id, text))
277+ .map(|face| face.id)
281+ .find(|face| self.supports(face.id, text))
282+ .map(|face| face.id)
291+ requested: &RequestedFont,
292+ metrics: &mut Metrics,
293+ runs: Option<&mut Vec<GlyphRun>>,
294+ ) -> HostResult<()> {
298+ .ok_or_else(|| failure("font face is unavailable"))?;
299+ let embolden = requested.weight >= 600 && selected.weight.0 < 600;
300+ let skew_x = if requested.style != Style::Normal && selected.style == Style::Normal {
306+ .with_face_data(id, |data, index| -> HostResult<()> {
307+ let face = Face::from_slice(data, index)
308+ .ok_or_else(|| failure("font could not be shaped"))?;
309+ let scale = requested.size / f64::from(face.units_per_em());
310+ let mut buffer = UnicodeBuffer::new();
311+ buffer.push_str(text);
312+ buffer.guess_segment_properties();
313+ let normal = self.features.get(&id).map(Vec::as_slice).unwrap_or(&[]);
314+ let mut caps = Vec::new();
315+ let features = if requested.small_caps {
316+ caps.extend_from_slice(normal);
318+ Feature::from_str("smcp")
319+ .map_err(|_| failure("invalid small-caps feature"))?,
325+ let glyphs = rustybuzz::shape(&face, features, buffer);
326+ let mut advance = 0i64;
327+ let mut y_advance = 0i64;
328+ let mut run = runs.as_ref().map(|_| GlyphRun {
330+ size: requested.size as f32,
333+ glyphs: Vec::with_capacity(glyphs.len()),
334+ positions: Vec::with_capacity(glyphs.len()),
336+ let total_advance: i64 = glyphs
339+ .map(|position| i64::from(position.x_advance))
341+ let origin = metrics.width - (total_advance.min(0) as f64) * scale;
342+ for (info, position) in glyphs.glyph_infos().iter().zip(glyphs.glyph_positions()) {
343+ if let Some(run) = &mut run {
344+ run.glyphs.push(info.glyph_id as u16);
345+ run.positions.push(Point::new(
346+ (origin + (advance + i64::from(position.x_offset)) as f64 * scale)
348+ (-(y_advance + i64::from(position.y_offset)) as f64 * scale) as f32,
351+ y_advance += i64::from(position.y_advance);
352+ advance += i64::from(position.x_advance);
353+ if let Some(bounds) = face
354+ .glyph_bounding_box(rustybuzz::ttf_parser::GlyphId(info.glyph_id as u16))
356+ metrics.ascent = metrics
358+ .max(f64::from(i32::from(bounds.y_max) + position.y_offset) * scale);
359+ metrics.descent = metrics
361+ .max(-f64::from(i32::from(bounds.y_min) + position.y_offset) * scale);
364+ metrics.width += (advance as f64).abs() * scale;
365+ if let (Some(runs), Some(run)) = (runs, run) {
370+ .ok_or_else(|| failure("font bytes are unavailable"))?
373+ fn raster(&mut self, id: u32) -> HostResult<&mut Raster> {
376+ .ok_or_else(|| failure(format!("unknown canvas surface: {id}")))
379+ pub fn create(&mut self, width: i32, height: i32) -> HostResult<u32> {
380+ rgba_size(width, height)?;
381+ let id = self.next_surface;
384+ .ok_or_else(|| failure("canvas handles exhausted"))?;
385+ let mut surface = surfaces::raster_n32_premul((width, height))
386+ .ok_or_else(|| failure(format!("could not allocate {width}x{height} canvas")))?;
387+ // Preserve an untouched root clip so reset can discard every user clip.
388+ surface.canvas().save();
389+ self.surfaces.insert(
393+ style: DrawStyle::default(),
397+ self.next_surface = next;
401+ pub fn release(&mut self, id: u32) -> HostResult<()> {
404+ .ok_or_else(|| failure(format!("unknown canvas surface: {id}")))?;
408+ pub fn reset(&mut self, id: u32) -> HostResult<()> {
409+ let raster = self.raster(id)?;
410+ let canvas = raster.surface.canvas();
411+ canvas.restore_to_count(1);
412+ canvas.reset_matrix();
413+ canvas.clear(skia_safe::Color::TRANSPARENT);
415+ raster.style = DrawStyle::default();
416+ raster.saved.clear();
420+ pub fn clear(&mut self, id: u32, color: Color) -> HostResult<()> {
421+ self.raster(id)?.surface.canvas().clear(skia_color(color));
425+ pub fn save(&mut self, id: u32) -> HostResult<()> {
426+ let raster = self.raster(id)?;
427+ raster.saved.push(raster.style.clone());
428+ raster.surface.canvas().save();
432+ pub fn restore(&mut self, id: u32) -> HostResult<()> {
433+ let raster = self.raster(id)?;
434+ raster.style = raster
437+ .ok_or_else(|| failure("canvas restore without matching save"))?;
438+ raster.surface.canvas().restore();
442+ pub fn set_transform(&mut self, id: u32, matrix: [f32; 6]) -> HostResult<()> {
443+ let matrix = affine(matrix)?;
444+ self.raster(id)?.surface.canvas().set_matrix(&matrix.into());
448+ pub fn transform(&mut self, id: u32, matrix: [f32; 6]) -> HostResult<()> {
449+ let matrix = affine(matrix)?;
450+ self.raster(id)?.surface.canvas().concat(&matrix);
454+ pub fn clip(&mut self, id: u32, path: &Path, even_odd: bool) -> HostResult<()> {
455+ // SkPath clones share their immutable point/verb storage.
456+ let mut clip = path.clone();
457+ clip.set_fill_type(if even_odd {
458+ PathFillType::EvenOdd
460+ PathFillType::Winding
465+ .clip_path(&clip, ClipOp::Intersect, true);
469+ pub fn set_blend(&mut self, id: u32, mode: &str) -> HostResult<()> {
470+ let blend = match mode {
471+ "source-over" => BlendMode::SrcOver,
472+ "difference" => BlendMode::Difference,
473+ "destination-out" => BlendMode::DstOut,
474+ "destination-in" => BlendMode::DstIn,
475+ "source-in" => BlendMode::SrcIn,
476+ "copy" => BlendMode::Src,
477+ _ => return Err(failure(format!("unknown canvas blend mode: {mode}"))),
479+ self.raster(id)?.style.blend = blend;
483+ pub fn set_blur(&mut self, id: u32, radius: f32) -> HostResult<()> {
484+ nonnegative(radius, "blur radius")?;
485+ self.raster(id)?.style.blur = radius;
489+ pub fn set_tint(&mut self, id: u32, tint: Option<Color>) -> HostResult<()> {
492+ color_filters::blend(skia_color(color), BlendMode::SrcIn)
493+ .ok_or_else(|| failure("could not create canvas tint"))
496+ self.raster(id)?.style.tint = filter;
500+ pub fn fill_path(&mut self, id: u32, path: &Path, color: Color) -> HostResult<()> {
501+ self.raster(id)?.draw(|canvas, mut paint| {
502+ paint.set_color(skia_color(color));
503+ canvas.draw_path(path, &paint);
507+ pub fn stroke_path(&mut self, id: u32, path: &Path, stroke: Stroke) -> HostResult<()> {
508+ let (join, cap) = stroke_style(&stroke)?;
509+ self.raster(id)?.draw(|canvas, mut paint| {
510+ set_stroke(&mut paint, &stroke, join, cap);
511+ canvas.draw_path(path, &paint);
515+ pub fn fill_stroke_path(
521+ ) -> HostResult<()> {
522+ // Validate the entire operation before painting its first component.
523+ stroke_style(&stroke)?;
524+ self.fill_path(id, path, color)?;
525+ self.stroke_path(id, path, stroke)
538+ ) -> HostResult<()> {
539+ finite(&[x, y], "text position")?;
541+ if !(0..=2).contains(&align) {
542+ return Err(failure(format!("unknown canvas text alignment: {align}")));
544+ if !(0..=3).contains(&baseline) {
545+ return Err(failure(format!("unknown canvas text baseline: {baseline}")));
547+ let mut runs = Vec::new();
548+ let (metrics, base, size) = self.shape_text(font, text, Some(&mut runs))?;
549+ let x = x - match align {
550+ 1 => metrics.width as f32 / 2.0,
551+ 2 => metrics.width as f32,
554+ for id in std::iter::once(base).chain(runs.iter().map(|run| run.id)) {
555+ if !self.typefaces.contains_key(&id) {
558+ .with_face_data(id, |data, index| {
559+ self.font_manager.new_from_data(Data::new_copy(data), index)
562+ .ok_or_else(|| failure("Skia could not load the shaped font face"))?;
563+ self.typefaces.insert(id, face);
566+ let typefaces = &self.typefaces;
567+ // Canvas baselines belong to the font, not to the current word's ink.
568+ // Otherwise a reveal from "H" to "Hg" moves the already-visible H.
569+ let y = if baseline == 2 {
572+ let (_, line) = Font::from_typeface(typefaces[&base].clone(), size as f32).metrics();
573+ y + match baseline {
575+ 1 => -(line.ascent + line.descent) / 2.0,
582+ .ok_or_else(|| failure(format!("unknown canvas surface: {id}")))?;
583+ raster.draw(|canvas, mut paint| {
584+ paint.set_color(skia_color(color));
586+ let mut font = Font::from_typeface(typefaces[&run.id].clone(), run.size);
587+ // Keep fractional Rustybuzz placement; no independent hinted advances.
588+ font.set_subpixel(true)
589+ .set_hinting(FontHinting::None)
590+ .set_embolden(run.embolden)
591+ .set_skew_x(run.skew_x);
592+ canvas.draw_glyphs_at(&run.glyphs, run.positions.as_slice(), (x, y), &font, &paint);
597+ pub fn circle_pattern(&mut self, id: u32, pattern: CirclePattern) -> HostResult<()> {
598+ finite(&pattern.rect, "pattern rectangle")?;
599+ finite(&pattern.origin, "pattern origin")?;
600+ nonnegative(pattern.radius, "pattern radius")?;
601+ nonnegative(pattern.rect[2], "pattern width")?;
602+ nonnegative(pattern.rect[3], "pattern height")?;
603+ if !pattern.tile.is_finite() || pattern.tile <= 0.0 {
604+ return Err(failure("pattern tile must be positive and finite"));
606+ // Record just one periodic tile, not one FFI call or picture per screen dot.
607+ let half = pattern.tile / 2.0;
608+ let tile = Rect::from_xywh(-half, -half, pattern.tile, pattern.tile);
609+ let mut recorder = PictureRecorder::new();
610+ let canvas = recorder.begin_recording(tile, false);
611+ canvas.clear(skia_color(pattern.background));
612+ let mut paint = Paint::default();
614+ .set_anti_alias(true)
615+ .set_color(skia_color(pattern.ink));
616+ // Neighbouring circles must contribute when a radius crosses a tile edge.
617+ let reach = ((pattern.radius as f64 / pattern.tile as f64) + 0.5).floor();
618+ if reach > i32::MAX as f64 {
620+ "pattern radius/tile ratio exceeds native coordinate range",
623+ let reach = reach as i32;
624+ for row in -reach..=reach {
625+ for column in -reach..=reach {
627+ (column as f32 * pattern.tile, row as f32 * pattern.tile),
633+ let picture = recorder
634+ .finish_recording_as_picture(None)
635+ .ok_or_else(|| failure("could not record circle tile"))?;
636+ let matrix = Matrix::translate((pattern.origin[0], pattern.origin[1]));
637+ let shader = picture.to_shader(
638+ (TileMode::Repeat, TileMode::Repeat),
643+ let [x, y, width, height] = pattern.rect;
644+ self.raster(id)?.draw(|canvas, mut paint| {
645+ paint.set_shader(shader);
646+ canvas.draw_rect(Rect::from_xywh(x, y, width, height), &paint);
650+ pub fn surface(&mut self, destination: u32, source: u32) -> HostResult<()> {
651+ self.raster(destination)?;
652+ // Raster snapshots share pixels until a writer needs copy-on-write. In
653+ // the normal distinct-surface case this draws without a full-frame copy.
654+ let image = self.raster(source)?.surface.image_snapshot();
655+ self.raster(destination)?.draw(|canvas, paint| {
656+ canvas.draw_image(&image, (0.0, 0.0), Some(&paint));
660+ pub fn png(&mut self, id: u32, path: &str) -> HostResult<()> {
665+ .ok_or_else(|| failure("canvas raster pixels are unavailable"))?;
666+ let mut file = std::fs::File::create(path)?;
667+ if !skia_safe::png_encoder::encode(&pixels, &mut file, &Default::default()) {
668+ return Err(failure(format!("could not encode canvas PNG: {path}")));
679+ ) -> HostResult<()> {
680+ let size = rgba_size(width, height)?;
681+ let raster = self.raster(id)?;
682+ if raster.surface.width() != width || raster.surface.height() != height {
683+ return Err(failure(format!(
684+ "canvas is {}x{}, expected {width}x{height}",
685+ raster.surface.width(),
686+ raster.surface.height(),
689+ if output.len() != size {
690+ return Err(failure(format!(
691+ "RGBA buffer has {} bytes, expected {size}",
695+ let info = ImageInfo::new(
697+ ColorType::RGBA8888,
698+ AlphaType::Unpremul,
703+ .read_pixels(&info, output, width as usize * 4, (0, 0))
705+ return Err(failure("could not read straight RGBA canvas pixels"));
711+impl Default for DrawStyle {
712+ fn default() -> Self {
714+ blend: BlendMode::SrcOver,
722+ fn draw(&mut self, draw: impl FnOnce(&Canvas, Paint)) -> HostResult<()> {
723+ let mut paint = Paint::default();
724+ paint.set_anti_alias(true);
725+ paint.set_blend_mode(self.style.blend);
726+ paint.set_color_filter(self.style.tint.clone());
727+ let canvas = self.surface.canvas();
728+ if self.style.blur == 0.0 {
729+ draw(canvas, paint);
731+ let filter = image_filters::blur(
732+ (self.style.blur, self.style.blur),
737+ .ok_or_else(|| failure("could not create canvas blur"))?;
738+ paint.set_image_filter(filter);
739+ // Establish the filter layer in device space, then restore the
740+ // geometry transform inside it. Sigma never inherits a zoom/shear.
741+ let matrix = canvas.local_to_device();
742+ let count = canvas.save();
743+ canvas.reset_matrix();
744+ canvas.save_layer(&SaveLayerRec::default().paint(&paint));
745+ canvas.set_matrix(&matrix);
746+ let mut content = Paint::default();
747+ content.set_anti_alias(true);
748+ draw(canvas, content);
749+ canvas.restore_to_count(count);
755+pub(crate) fn path_from_values(
756+ mut values: impl Iterator<Item = HostResult<f64>>,
757+) -> HostResult<Path> {
758+ fn coordinate(values: &mut impl Iterator<Item = HostResult<f64>>) -> HostResult<f32> {
761+ .ok_or_else(|| failure("truncated canvas path command"))??;
762+ let coordinate = value as f32;
763+ if !coordinate.is_finite() {
765+ "canvas path coordinates must be finite native numbers",
770+ fn point(values: &mut impl Iterator<Item = HostResult<f64>>) -> HostResult<Point> {
771+ Ok(Point::new(coordinate(values)?, coordinate(values)?))
773+ let mut builder = PathBuilder::new();
774+ let mut has_move = false;
775+ while let Some(tag) = values.next() {
777+ if tag != 1.0 && !has_move && (2.0..=5.0).contains(&tag) {
778+ return Err(failure("canvas path must begin with move-to"));
782+ builder.move_to(point(&mut values)?);
786+ builder.line_to(point(&mut values)?);
789+ let control = point(&mut values)?;
790+ builder.quad_to(control, point(&mut values)?);
793+ let first = point(&mut values)?;
794+ let second = point(&mut values)?;
795+ builder.cubic_to(first, second, point(&mut values)?);
800+ _ => return Err(failure(format!("unknown canvas path command: {tag}"))),
803+ Ok(builder.detach())
806+fn skia_color([r, g, b, a]: Color) -> skia_safe::Color {
807+ skia_safe::Color::from_argb(a, r, g, b)
810+fn finite(values: &[f32], name: &str) -> HostResult<()> {
811+ if values.iter().all(|value| value.is_finite()) {
814+ Err(failure(format!(
815+ "{name} must contain finite native numbers"
820+fn nonnegative(value: f32, name: &str) -> HostResult<()> {
821+ if value.is_finite() && value >= 0.0 {
824+ Err(failure(format!("{name} must be finite and nonnegative")))
828+fn affine([a, b, c, d, e, f]: [f32; 6]) -> HostResult<Matrix> {
829+ finite(&[a, b, c, d, e, f], "canvas transform")?;
830+ Ok(Matrix::new_all(a, c, e, b, d, f, 0.0, 0.0, 1.0))
833+fn stroke_style(stroke: &Stroke) -> HostResult<(paint::Join, paint::Cap)> {
834+ nonnegative(stroke.width, "stroke width")?;
835+ let join = match stroke.join {
836+ 0 => paint::Join::Round,
837+ 1 => paint::Join::Bevel,
838+ 2 => paint::Join::Miter,
839+ value => return Err(failure(format!("unknown canvas stroke join: {value}"))),
841+ let cap = match stroke.cap {
842+ 0 => paint::Cap::Butt,
843+ 1 => paint::Cap::Round,
844+ 2 => paint::Cap::Square,
845+ value => return Err(failure(format!("unknown canvas stroke cap: {value}"))),
850+fn set_stroke(paint: &mut Paint, stroke: &Stroke, join: paint::Join, cap: paint::Cap) {
851+ paint.set_color(skia_color(stroke.color));
852+ paint.set_style(PaintStyle::Stroke);
853+ paint.set_stroke_width(stroke.width);
854+ paint.set_stroke_join(join);
855+ paint.set_stroke_cap(cap);
856+ paint.set_stroke_miter(4.0);
859+fn rgba_size(width: i32, height: i32) -> HostResult<usize> {
860+ if width <= 0 || height <= 0 {
861+ return Err(failure("canvas dimensions must be positive"));
864+ .checked_mul(height as usize)
865+ .and_then(|size| size.checked_mul(4))
866+ .ok_or_else(|| failure("canvas dimensions overflow the RGBA buffer size"))
869+fn font_size(value: &str) -> HostResult<f64> {
870+ let length = svgtypes::Length::from_str(value)?;
871+ let scale = match length.unit {
872+ svgtypes::LengthUnit::None | svgtypes::LengthUnit::Px => 1.0,
873+ svgtypes::LengthUnit::Pt => 96.0 / 72.0,
874+ svgtypes::LengthUnit::Pc => 16.0,
875+ svgtypes::LengthUnit::In => 96.0,
876+ svgtypes::LengthUnit::Cm => 96.0 / 2.54,
877+ svgtypes::LengthUnit::Mm => 96.0 / 25.4,
880+ "native font measurements require an absolute CSS font size",
884+ let size = length.number * scale;
885+ if size > 0.0 && (size as f32).is_finite() && (size as f32) > 0.0 {
889+ "font size must be positive and finite in native coordinates",