Skip to main content

alpha2_extensions/svg/
mod.rs

1//! Static SVG, rasterized by resvg at the size it is shown.
2//!
3//! Only resources a host registers are read: fonts from the resolver (never
4//! the system's, which differ between machines and are absent in a
5//! browser), and images from `data:` URLs or registered locators (PNG,
6//! JPEG, GIF, WebP, or SVG). Text without a font, an image that is not
7//! registered, and one registered in another format are reported rather
8//! than silently left out.
9//!
10//! Neither usvg nor resvg limits its own work, so what each would cost is
11//! counted before it runs, and a document past a budget is refused (`too
12//! large`), at probe and open alike:
13//! - the depth its elements nest, from its text before the XML parser,
14//!   which recurses once a level ([`markup`]), within [`MAX_NESTING`];
15//! - usvg's conversion of the parsed XML: `<use>` copies, style rules
16//!   matched against every element, each element's ancestry
17//!   ([`document`]), within [`MAX_READING`] steps;
18//! - the SVG text it parses, its own and its nested SVG images' (unpacked,
19//!   each time one is drawn), within [`MAX_SVG_DOCUMENT_BYTES`] together;
20//! - resvg's drawing ([`render_cost`]): at the document's own size when it
21//!   opens, and again at each size a render asks for.
22
23mod document;
24mod markup;
25mod path_data;
26mod render_cost;
27
28use std::io::Read as _;
29use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
30use std::sync::{Arc, Mutex, PoisonError};
31
32use alpha2_time::MediaTime;
33use resvg::tiny_skia::{Pixmap, Transform};
34use resvg::usvg::{
35    self, FontResolver, ImageHrefResolver, ImageKind, Node, Options, Tree, roxmltree,
36};
37
38use alpha2_model::{MAX_SVG_DOCUMENT_BYTES, ProviderSource};
39
40use crate::api::{
41    Diagnostic, ExtensionError, Format, FramePixels, ProbeReport, ResourceResolver, Timing,
42    VisualFrame, VisualProvider, VisualSource,
43};
44use crate::budget::{check_canvas, check_document_size, check_image_pixels};
45
46/// Reads static SVG documents.
47pub struct SvgProvider;
48
49impl VisualProvider for SvgProvider {
50    fn name(&self) -> &'static str {
51        "svg"
52    }
53
54    fn probe(&self, bytes: &[u8]) -> Option<Result<ProbeReport, ExtensionError>> {
55        if !looks_like_svg(bytes) {
56            return None;
57        }
58        Some(
59            check_document_size(&ProviderSource::Svg, bytes, bytes.len() as u64)
60                .and_then(|()| parse(bytes, &crate::api::NoResources))
61                .map(|(_, report)| report),
62        )
63    }
64
65    fn open(
66        &self,
67        bytes: Arc<[u8]>,
68        _source: &ProviderSource,
69        resolver: &dyn ResourceResolver,
70    ) -> Result<Box<dyn VisualSource>, ExtensionError> {
71        if !looks_like_svg(&bytes) {
72            return Err(ExtensionError::Mismatch("not an SVG document".into()));
73        }
74        check_document_size(&ProviderSource::Svg, &bytes, bytes.len() as u64)?;
75        let (tree, report) = parse(&bytes, resolver)?;
76        Ok(Box::new(SvgSource {
77            tree,
78            report,
79            last: None,
80        }))
81    }
82}
83
84/// Whether the bytes are XML whose root element is `svg`. Before it may
85/// come a byte order mark, whitespace, the XML declaration and other
86/// processing instructions, comments, and a doctype, its internal subset
87/// included (as Adobe Illustrator writes one), however long they are.
88fn looks_like_svg(bytes: &[u8]) -> bool {
89    let mut rest = bytes.strip_prefix(b"\xef\xbb\xbf").unwrap_or(bytes);
90    loop {
91        rest = rest.trim_ascii_start();
92        let skipped = if let Some(after) = rest.strip_prefix(b"<?") {
93            after_closing(after, b"?>")
94        } else if let Some(after) = rest.strip_prefix(b"<!--") {
95            after_closing(after, b"-->")
96        } else if let Some(after) = rest.strip_prefix(b"<!DOCTYPE") {
97            after_doctype(after)
98        } else {
99            return rest.strip_prefix(b"<svg").is_some_and(|after| {
100                after
101                    .first()
102                    .is_some_and(|next| next.is_ascii_whitespace() || matches!(next, b'>' | b'/'))
103            });
104        };
105        let Some(after) = skipped else {
106            return false;
107        };
108        rest = after;
109    }
110}
111
112/// What follows the first `closing` in `bytes`, or `None` without one.
113fn after_closing<'a>(bytes: &'a [u8], closing: &[u8]) -> Option<&'a [u8]> {
114    let at = bytes
115        .windows(closing.len())
116        .position(|window| window == closing)?;
117    Some(&bytes[at + closing.len()..])
118}
119
120/// What follows a doctype declaration whose `<!DOCTYPE` came before
121/// `bytes`, or `None` when it does not end. Its quoted literals, and its
122/// internal subset's declarations, comments, and processing instructions,
123/// may hold a `>` that does not end it.
124fn after_doctype(mut bytes: &[u8]) -> Option<&[u8]> {
125    let mut subset = false;
126    loop {
127        if let Some(after) = bytes.strip_prefix(b"<!--") {
128            bytes = after_closing(after, b"-->")?;
129        } else if let Some(after) = bytes.strip_prefix(b"<?") {
130            bytes = after_closing(after, b"?>")?;
131        } else {
132            let (&first, after) = bytes.split_first()?;
133            bytes = match first {
134                b'"' | b'\'' => after_closing(after, &[first])?,
135                b'[' if !subset => {
136                    subset = true;
137                    after
138                }
139                b']' if subset => {
140                    subset = false;
141                    after
142                }
143                b'>' if !subset => return Some(after),
144                _ => after,
145            };
146        }
147    }
148}
149
150/// Deepest the elements of an SVG document nest, the SVG images drawn inside
151/// it included (each from the depth it is drawn at), with every `<use>` copy
152/// in place. The XML parser, usvg, and resvg each recurse once a level: at
153/// this depth they take well under a quarter of a 1 MiB stack (a browser's,
154/// natively a release build overflows 1 MiB between 600 and 1,000 levels of
155/// groups); usvg itself stops at 1,024. Documents meant to be drawn nest a
156/// few tens deep.
157pub(crate) const MAX_NESTING: usize = 256;
158
159/// Most steps reading an SVG document may take, its SVG images included:
160/// each style rule tested against each element (backtracking through its
161/// ancestors) and each declaration applied, each `<use>` copy's loop check,
162/// and three for each level of each element's ancestry, which usvg walks to
163/// resolve its properties (about 18 ns natively). About a second natively.
164pub(crate) const MAX_READING: u64 = 1 << 27;
165
166/// What reading one document, with the SVG images inside it, may still
167/// take. Shared by the image resolver, which reads the nested documents
168/// while usvg converts the one that names them.
169struct Reading {
170    /// Bytes of SVG text still to be parsed: the document's own and its
171    /// nested documents', unpacked.
172    text: AtomicUsize,
173    /// Steps still to be taken.
174    steps: AtomicU64,
175    /// Levels the documents being converted take, below the one being read.
176    nesting: AtomicUsize,
177    /// Why a nested document could not be read within these, which refuses
178    /// the document that draws it.
179    refused: Mutex<Option<ExtensionError>>,
180}
181
182impl Reading {
183    fn new() -> Self {
184        Self {
185            text: AtomicUsize::new(MAX_SVG_DOCUMENT_BYTES),
186            steps: AtomicU64::new(MAX_READING),
187            nesting: AtomicUsize::new(0),
188            refused: Mutex::new(None),
189        }
190    }
191
192    fn refuse(&self, error: ExtensionError) {
193        self.refused
194            .lock()
195            .unwrap_or_else(PoisonError::into_inner)
196            .get_or_insert(error);
197    }
198
199    fn refusal(&self) -> Option<ExtensionError> {
200        self.refused
201            .lock()
202            .unwrap_or_else(PoisonError::into_inner)
203            .take()
204    }
205
206    /// Takes `length` bytes of text from what is left.
207    fn take_text(&self, length: usize) -> Result<(), ExtensionError> {
208        let left = self.text.load(Ordering::Relaxed);
209        let left = left.checked_sub(length).ok_or_else(too_much_text)?;
210        self.text.store(left, Ordering::Relaxed);
211        Ok(())
212    }
213}
214
215fn too_much_text() -> ExtensionError {
216    ExtensionError::Budget(format!(
217        "the SVG and the SVG images it draws are more than {MAX_SVG_DOCUMENT_BYTES} bytes of text together"
218    ))
219}
220
221/// Parses SVG `text` with `options` within what `reading` has left: its
222/// nesting, from the text; its conversion, from the XML; then usvg's
223/// conversion, with the levels this document takes set aside for the SVG
224/// images it draws.
225fn read_document(
226    text: &[u8],
227    options: &Options,
228    reading: &Reading,
229) -> Result<Tree, ExtensionError> {
230    let malformed = |error: usvg::Error| ExtensionError::Malformed(error.to_string());
231    reading.take_text(text.len())?;
232    let text = std::str::from_utf8(text).map_err(|_| malformed(usvg::Error::NotAnUtf8Str))?;
233    let base = reading.nesting.load(Ordering::Relaxed);
234    let limit = MAX_NESTING.saturating_sub(base);
235    markup::nesting(text, limit)?;
236    let xml = roxmltree::Document::parse_with_options(
237        text,
238        roxmltree::ParsingOptions {
239            allow_dtd: true,
240            ..roxmltree::ParsingOptions::default()
241        },
242    )
243    .map_err(|error| malformed(usvg::Error::ParsingFailed(error)))?;
244    let steps = reading.steps.load(Ordering::Relaxed);
245    let conversion = document::conversion(&xml, steps, limit)?;
246    reading
247        .steps
248        .store(steps - conversion.steps, Ordering::Relaxed);
249    reading
250        .nesting
251        .store(base + conversion.depth, Ordering::Relaxed);
252    let tree = Tree::from_xmltree(&xml, options);
253    reading.nesting.store(base, Ordering::Relaxed);
254    tree.map_err(malformed)
255}
256
257/// Parses a document with only `resolver`'s fonts and images, checks its
258/// canvas, decoded image sizes, and what drawing it at its own size costs,
259/// and reports what it could not draw.
260fn parse(
261    bytes: &[u8],
262    resolver: &dyn ResourceResolver,
263) -> Result<(Tree, ProbeReport), ExtensionError> {
264    let registered_fonts = resolver.fonts();
265    let missing_fonts = Mutex::new(Vec::<String>::new());
266    // What the images the document names came to, in the order usvg asked for them.
267    let image_reports = Mutex::new(Vec::<Diagnostic>::new());
268    let reading = Reading::new();
269    let options = Options {
270        // Empty until text asks for a font: a document without text loads none.
271        fontdb: Arc::new(usvg::fontdb::Database::new()),
272        font_resolver: font_resolver(&registered_fonts, &missing_fonts),
273        image_href_resolver: image_resolver(resolver, &image_reports, &reading),
274        ..Options::default()
275    };
276    let tree = read_document(bytes, &options, &reading);
277    drop(options);
278    if let Some(refusal) = reading.refusal() {
279        return Err(refusal);
280    }
281    let tree = tree?;
282    let size = tree.size().to_int_size();
283    check_canvas(size.width(), size.height())?;
284    check_image_pixels(image_pixels(tree.root()))?;
285    render_cost::check(&tree, size.width(), size.height())?;
286    let mut diagnostics = Vec::new();
287    let missing_fonts = missing_fonts
288        .into_inner()
289        .unwrap_or_else(PoisonError::into_inner);
290    if !missing_fonts.is_empty() {
291        diagnostics.push(Diagnostic::MissingFont {
292            families: missing_fonts.join(", "),
293        });
294    }
295    for diagnostic in image_reports
296        .into_inner()
297        .unwrap_or_else(PoisonError::into_inner)
298    {
299        if !diagnostics.contains(&diagnostic) {
300            diagnostics.push(diagnostic);
301        }
302    }
303    let report = ProbeReport {
304        format: Format::Svg,
305        width: size.width(),
306        height: size.height(),
307        timing: Timing::Static,
308        loop_count: None,
309        items: Vec::new(),
310        item: None,
311        color: None,
312        diagnostics,
313    };
314    Ok((tree, report))
315}
316
317/// Selects fonts from `registered`, loaded into the document's database on
318/// the first request, and records in `missing` the families of each request
319/// that matches none.
320///
321/// Every text usvg draws asks here, once per place it is drawn (through
322/// `<use>`, a clip path, a mask, a pattern, or a nested SVG), and text it
323/// leaves out (unused definitions, `display="none"`) never does: a text no
324/// registered font matches is one usvg leaves out.
325fn font_resolver<'a>(
326    registered: &'a [Arc<[u8]>],
327    missing: &'a Mutex<Vec<String>>,
328) -> FontResolver<'a> {
329    let select_font = FontResolver::default_font_selector();
330    FontResolver {
331        select_font: Box::new(move |font, fontdb| {
332            if fontdb.is_empty() {
333                let fontdb = Arc::make_mut(fontdb);
334                for font in registered {
335                    // Shared with the resolver, never copied: a source keeps its fonts as long
336                    // as its tree.
337                    fontdb
338                        .load_font_source(usvg::fontdb::Source::Binary(Arc::new(Arc::clone(font))));
339                }
340            }
341            let found = select_font(font, fontdb);
342            if found.is_none() {
343                let mut missing = missing.lock().unwrap_or_else(PoisonError::into_inner);
344                for family in font.families() {
345                    let family = match family {
346                        usvg::FontFamily::Named(name) => name.clone(),
347                        generic => generic.to_string(),
348                    };
349                    if !missing.contains(&family) {
350                        missing.push(family);
351                    }
352                }
353            }
354            found
355        }),
356        select_fallback: FontResolver::default_fallback_selector(),
357    }
358}
359
360/// Reads `data:` images, and other hrefs from `resolver`, as usvg asks for
361/// them with the href XML reads (entities decoded, however the attribute
362/// is written); what is not drawn is recorded in `reports`. SVG images are
363/// read within what `reading` has left, as the document that draws them.
364fn image_resolver<'a>(
365    resolver: &'a dyn ResourceResolver,
366    reports: &'a Mutex<Vec<Diagnostic>>,
367    reading: &'a Reading,
368) -> ImageHrefResolver<'a> {
369    let default_data = ImageHrefResolver::default_data_resolver();
370    let report = move |diagnostic| {
371        reports
372            .lock()
373            .unwrap_or_else(PoisonError::into_inner)
374            .push(diagnostic);
375    };
376    ImageHrefResolver {
377        // Nested SVG documents resolve their `data:` images through this too.
378        resolve_data: Box::new(move |mime, data, options| {
379            // What usvg's own resolver reads as SVG: the SVG type, and plain
380            // text that is not a raster image.
381            let svg = mime == "image/svg+xml"
382                || (mime == "text/plain" && imagesize::image_type(&data).is_err());
383            if svg {
384                return nested_svg(&data, options, reading);
385            }
386            let kind = default_data(mime, data, options)?;
387            within_canvas(kind, || format!("data:{mime}"), reports)
388        }),
389        resolve_string: Box::new(move |href, options| {
390            let unreadable = |reason: &str| {
391                report(Diagnostic::UnreadableImage {
392                    href: href.to_owned(),
393                    reason: reason.to_owned(),
394                });
395                None
396            };
397            let Ok(bytes) = resolver.bytes(href) else {
398                report(Diagnostic::MissingImage {
399                    href: href.to_owned(),
400                });
401                return None;
402            };
403            let data = || Arc::new(bytes.to_vec());
404            let raster = match registered_image(&bytes) {
405                Some(Registered::Png) => ImageKind::PNG(data()),
406                Some(Registered::Jpeg) => ImageKind::JPEG(data()),
407                Some(Registered::Gif) => ImageKind::GIF(data()),
408                Some(Registered::Webp) => ImageKind::WEBP(data()),
409                // Read as a `data:` SVG is: its own external images are not loaded, as SVG
410                // defines for a document used as an image.
411                Some(Registered::Svg) => {
412                    return nested_svg(&bytes, options, reading)
413                        .or_else(|| unreadable("the SVG document does not parse"));
414                }
415                None => return unreadable("not a PNG, JPEG, GIF, WebP, or SVG image"),
416            };
417            within_canvas(raster, || href.to_owned(), reports)
418        }),
419    }
420}
421
422/// An SVG document drawn as an image, plain or gzipped, read as usvg reads
423/// one (its own external images are not loaded, as SVG defines for a
424/// document used as an image) within what `reading` has left. One past it
425/// refuses the document that draws it; one that does not parse is left out.
426fn nested_svg(data: &[u8], options: &Options, reading: &Reading) -> Option<ImageKind> {
427    let unpacked;
428    let text = if data.starts_with(&[0x1f, 0x8b]) {
429        // Unpacked only as far as there is text left to read.
430        let left = reading.text.load(Ordering::Relaxed);
431        let mut text = Vec::new();
432        let read = flate2::read::GzDecoder::new(data)
433            .take(u64::try_from(left).unwrap_or(u64::MAX).saturating_add(1))
434            .read_to_end(&mut text);
435        if read.is_err() {
436            return None;
437        }
438        if text.len() > left {
439            reading.refuse(too_much_text());
440            return None;
441        }
442        unpacked = text;
443        &unpacked[..]
444    } else {
445        data
446    };
447    let nested = Options {
448        resources_dir: None,
449        dpi: options.dpi,
450        font_size: options.font_size,
451        languages: options.languages.clone(),
452        shape_rendering: options.shape_rendering,
453        text_rendering: options.text_rendering,
454        image_rendering: options.image_rendering,
455        default_size: options.default_size,
456        image_href_resolver: ImageHrefResolver {
457            resolve_data: Box::new(|mime, data, nested| {
458                (options.image_href_resolver.resolve_data)(mime, data, nested)
459            }),
460            resolve_string: Box::new(|_, _| None),
461        },
462        fontdb: options.fontdb.clone(),
463        font_resolver: FontResolver {
464            select_font: Box::new(|font, fontdb| (options.font_resolver.select_font)(font, fontdb)),
465            select_fallback: Box::new(|c, used, fontdb| {
466                (options.font_resolver.select_fallback)(c, used, fontdb)
467            }),
468        },
469        ..Options::default()
470    };
471    match read_document(text, &nested, reading) {
472        Ok(tree) => Some(ImageKind::SVG(tree)),
473        Err(error @ ExtensionError::Budget(_)) => {
474            reading.refuse(error);
475            None
476        }
477        Err(_) => None,
478    }
479}
480
481/// `kind`, unless it is a raster image whose header declares a size larger
482/// than a provider opens: resvg decodes it at that size on every render,
483/// before anything is drawn. Such an image is reported missing, as `href`.
484fn within_canvas(
485    kind: ImageKind,
486    href: impl FnOnce() -> String,
487    reports: &Mutex<Vec<Diagnostic>>,
488) -> Option<ImageKind> {
489    let (ImageKind::JPEG(data)
490    | ImageKind::PNG(data)
491    | ImageKind::GIF(data)
492    | ImageKind::WEBP(data)) = &kind
493    else {
494        return Some(kind);
495    };
496    // A header usvg cannot size is left out by usvg itself.
497    let declared = imagesize::blob_size(data).ok()?;
498    let side = |value: usize| u32::try_from(value).unwrap_or(u32::MAX);
499    if check_canvas(side(declared.width), side(declared.height)).is_err() {
500        reports
501            .lock()
502            .unwrap_or_else(PoisonError::into_inner)
503            .push(Diagnostic::MissingImage { href: href() });
504        return None;
505    }
506    Some(kind)
507}
508
509/// The pixels the raster images `group` draws decode to, each counted every
510/// time it is drawn: resvg decodes an image each time it draws it, so one
511/// drawn through `<use>`, a pattern, a mask, or a nested SVG counts once per
512/// place it is drawn.
513fn image_pixels(group: &usvg::Group) -> u64 {
514    let mut pixels = 0_u64;
515    for node in group.children() {
516        match node {
517            Node::Group(inner) => pixels = pixels.saturating_add(image_pixels(inner)),
518            Node::Image(image) => pixels = pixels.saturating_add(raster_pixels(image.kind())),
519            Node::Path(_) | Node::Text(_) => {}
520        }
521        // Clip paths, masks, filter images, patterns, and a nested SVG's own tree.
522        node.subroots(|root| pixels = pixels.saturating_add(image_pixels(root)));
523    }
524    pixels
525}
526
527/// The pixels a raster image's header declares; none for a nested SVG,
528/// whose own images are counted where it is drawn.
529fn raster_pixels(kind: &ImageKind) -> u64 {
530    match kind {
531        ImageKind::JPEG(data)
532        | ImageKind::PNG(data)
533        | ImageKind::GIF(data)
534        | ImageKind::WEBP(data) => imagesize::blob_size(data).map_or(0, |size| {
535            u64::try_from(size.width)
536                .unwrap_or(u64::MAX)
537                .saturating_mul(u64::try_from(size.height).unwrap_or(u64::MAX))
538        }),
539        ImageKind::SVG(_) => 0,
540    }
541}
542
543/// What a registered image is.
544enum Registered {
545    Png,
546    Jpeg,
547    Gif,
548    Webp,
549    /// An SVG document, plain or gzipped.
550    Svg,
551}
552
553/// What registered image bytes are, by their signature; `None` for a
554/// format an SVG image is not drawn from.
555fn registered_image(bytes: &[u8]) -> Option<Registered> {
556    if bytes.starts_with(b"\x89PNG") {
557        Some(Registered::Png)
558    } else if bytes.starts_with(&[0xff, 0xd8]) {
559        Some(Registered::Jpeg)
560    } else if bytes.starts_with(b"GIF8") {
561        Some(Registered::Gif)
562    } else if bytes.len() > 12 && &bytes[..4] == b"RIFF" && &bytes[8..12] == b"WEBP" {
563        Some(Registered::Webp)
564    } else if looks_like_svg(bytes) || bytes.starts_with(&[0x1f, 0x8b]) {
565        Some(Registered::Svg)
566    } else {
567        None
568    }
569}
570
571/// An open SVG document, rasterized at the size each render asks for.
572struct SvgSource {
573    tree: Tree,
574    report: ProbeReport,
575    /// The last rasterization, reused while the size holds.
576    last: Option<VisualFrame>,
577}
578
579impl VisualSource for SvgSource {
580    fn report(&self) -> &ProbeReport {
581        &self.report
582    }
583
584    fn render(&mut self, _time: MediaTime, fit: (u32, u32)) -> Result<VisualFrame, ExtensionError> {
585        let size = self.tree.size();
586        let (width, height) = fitted(size.width(), size.height(), fit);
587        if let Some(last) = self
588            .last
589            .as_ref()
590            .filter(|last| (last.width, last.height) == (width, height))
591        {
592            return Ok(last.clone());
593        }
594        render_cost::check(&self.tree, width, height)?;
595        let mut pixmap = Pixmap::new(width, height).ok_or_else(|| {
596            ExtensionError::Budget(format!("cannot rasterize at {width}x{height}"))
597        })?;
598        // Sizes are within MAX_CANVAS_SIDE, which f32 holds exactly.
599        #[allow(clippy::cast_precision_loss)]
600        let transform =
601            Transform::from_scale(width as f32 / size.width(), height as f32 / size.height());
602        resvg::render(&self.tree, transform, &mut pixmap.as_mut());
603        let frame = VisualFrame {
604            width,
605            height,
606            pixels: FramePixels::Rgba8(pixmap.take().into()),
607            index: 0,
608            start: MediaTime::from_ticks(i64::MIN),
609            end: None,
610            diagnostics: Vec::new(),
611        };
612        self.last = Some(frame.clone());
613        Ok(frame)
614    }
615
616    fn resident_bytes(&self) -> usize {
617        self.last.as_ref().map_or(0, |last| last.pixels.byte_len())
618    }
619}
620
621/// The largest whole-pixel size of a `width` by `height` picture that fits in
622/// `fit`, keeping its shape; its own size when `fit` is empty.
623// Every size is positive and within MAX_CANVAS_SIDE, which f32 holds exactly.
624#[allow(
625    clippy::cast_precision_loss,
626    clippy::cast_possible_truncation,
627    clippy::cast_sign_loss
628)]
629fn fitted(width: f32, height: f32, fit: (u32, u32)) -> (u32, u32) {
630    if fit.0 == 0 || fit.1 == 0 {
631        return (
632            width.round().max(1.0) as u32,
633            height.round().max(1.0) as u32,
634        );
635    }
636    let scale = (fit.0 as f32 / width).min(fit.1 as f32 / height);
637    (
638        (width * scale).round().clamp(1.0, fit.0 as f32) as u32,
639        (height * scale).round().clamp(1.0, fit.1 as f32) as u32,
640    )
641}