image_rs/
yew.rs

1#![doc = include_str!("../YEW.md")]
2
3use crate::common::{
4    AriaLive, AriaPressed, CrossOrigin, Decoding, FetchPriority, Layout, Loading, ObjectFit,
5    Position, ReferrerPolicy,
6};
7use gloo_net::http::Request;
8use wasm_bindgen_futures::spawn_local;
9use web_sys::IntersectionObserverEntry;
10use web_sys::js_sys;
11use web_sys::wasm_bindgen::JsCast;
12use web_sys::wasm_bindgen::JsValue;
13use web_sys::wasm_bindgen::prelude::*;
14use web_sys::{IntersectionObserver, IntersectionObserverInit, RequestCache};
15use yew::prelude::*;
16
17/// Properties for the `Image` component.
18///
19/// The `Image` component allows you to display an image with various customization options
20/// for layout, styling, and behavior. It supports fallback images, lazy loading, and custom
21/// callbacks for error handling and loading completion.
22///
23/// This component is highly flexible, providing support for multiple image layouts,
24/// object-fit, object-position, ARIA attributes, and more.
25///
26/// # See Also
27/// - [MDN img Element](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img)
28#[derive(Properties, Clone, PartialEq)]
29pub struct ImageProps {
30    /// The source URL of the image.
31    ///
32    /// This is the URL of the image to be displayed. This property is required for loading
33    /// an image. If not provided, the image will not be displayed.
34    #[prop_or_default]
35    pub src: &'static str,
36
37    /// The alternative text for the image.
38    ///
39    /// This is the alt text for the image, which is used for accessibility purposes.
40    /// If not provided, the alt text will be empty.
41    #[prop_or_default]
42    pub alt: &'static str,
43
44    /// Optional fallback image.
45    ///
46    /// This image will be displayed if the main image fails to load. If not provided,
47    /// the image will attempt to load without a fallback.
48    #[prop_or_default]
49    pub fallback_src: &'static str,
50
51    /// The width of the image.
52    ///
53    /// Specifies the width of the image in pixels. It is typically used for responsive
54    /// layouts. Defaults to an empty string if not provided.
55    #[prop_or_default]
56    pub width: &'static str,
57
58    /// The height of the image.
59    ///
60    /// Specifies the height of the image in pixels. Like `width`, it is often used for
61    /// responsive layouts. Defaults to an empty string if not provided.
62    #[prop_or_default]
63    pub height: &'static str,
64
65    // Common props
66    /// The style attribute for the image.
67    ///
68    /// Allows you to apply custom inline CSS styles to the image. Defaults to an empty string.
69    #[prop_or_default]
70    pub style: &'static str,
71
72    /// The CSS class for the image.
73    ///
74    /// This can be used to apply custom CSS classes to the image for styling purposes.
75    /// Defaults to an empty string if not provided.
76    #[prop_or_default]
77    pub class: &'static str,
78
79    /// The sizes attribute for the image.
80    ///
81    /// This is used to define different image sizes for different viewport widths, helping
82    /// with responsive images. Defaults to an empty string if not provided.
83    #[prop_or_default]
84    pub sizes: &'static str,
85
86    /// The quality attribute for the image.
87    ///
88    /// Allows you to set the quality of the image (e.g., "low", "medium", "high"). Defaults
89    /// to an empty string if not provided.
90    #[prop_or_default]
91    pub quality: &'static str,
92
93    /// Indicates if the image should have priority loading.
94    ///
95    /// This controls whether the image should be loaded eagerly (immediately) or lazily
96    /// (when it enters the viewport). Defaults to `false`.
97    #[prop_or_default]
98    pub loading: Loading,
99
100    /// The placeholder attribute for the image.
101    ///
102    /// Allows you to specify a placeholder image URL or data URL to show while the main
103    /// image is loading. Defaults to an empty string.
104    #[prop_or_default]
105    pub placeholder: &'static str,
106
107    /// Callback function for handling loading completion.
108    ///
109    /// This callback is triggered once the image has finished loading. This is useful for
110    /// actions that should happen after the image has been fully loaded, such as hiding
111    /// a loading spinner. Defaults to a no-op.
112    #[prop_or_default]
113    pub on_load: Callback<()>,
114
115    // Advanced Props
116    /// The object-fit attribute for the image.
117    ///
118    /// Determines how the image should be resized to fit its container. Common values include
119    /// "contain", "cover", "fill", etc. Defaults to an empty string.
120    #[prop_or_default]
121    pub object_fit: ObjectFit,
122
123    /// The object-position attribute for the image.
124    ///
125    /// Specifies how the image should be positioned within its container when `object-fit` is set.
126    /// The available options are:
127    /// - `Position::Center`: Centers the image within the container.
128    /// - `Position::Top`: Aligns the image to the top of the container.
129    /// - `Position::Bottom`: Aligns the image to the bottom of the container.
130    /// - `Position::Left`: Aligns the image to the left of the container.
131    /// - `Position::Right`: Aligns the image to the right of the container.
132    /// - `Position::TopLeft`: Aligns the image to the top-left of the container.
133    /// - `Position::TopRight`: Aligns the image to the top-right of the container.
134    /// - `Position::BottomLeft`: Aligns the image to the bottom-left of the container.
135    /// - `Position::BottomRight`: Aligns the image to the bottom-right of the container.
136    ///
137    /// Defaults to `Position::Center`.
138    #[prop_or_default]
139    pub object_position: Position,
140
141    /// Callback function for handling errors during image loading.
142    ///
143    /// This callback is triggered if the image fails to load, allowing you to handle
144    /// error states (e.g., displaying a fallback image or showing an error message).
145    #[prop_or_default]
146    pub on_error: Callback<String>,
147
148    /// The decoding attribute for the image.
149    ///
150    /// Specifies how the image should be decoded. The available options are:
151    /// - `Decoding::Auto`: The image decoding behavior is automatically decided by the browser.
152    /// - `Decoding::Sync`: The image is decoded synchronously (blocking other tasks).
153    /// - `Decoding::Async`: The image is decoded asynchronously (non-blocking).
154    ///
155    /// Defaults to `Decoding::Auto`.
156    #[prop_or_default]
157    pub decoding: Decoding,
158
159    /// The blur data URL for placeholder image.
160    ///
161    /// This is used to display a low-quality blurred version of the image while the full
162    /// image is loading. Defaults to an empty string.
163    #[prop_or_default]
164    pub blur_data_url: &'static str,
165
166    /// The lazy boundary for lazy loading.
167    ///
168    /// Defines the distance (in pixels) from the viewport at which the image should start
169    /// loading. Defaults to an empty string.
170    #[prop_or_default]
171    pub lazy_boundary: &'static str,
172
173    /// Indicates if the image should be unoptimized.
174    ///
175    /// If set to `true`, the image will be loaded without any optimization applied (e.g.,
176    /// no resizing or compression). Defaults to `false`.
177    #[prop_or_default]
178    pub unoptimized: bool,
179
180    /// Image layout.
181    ///
182    /// Specifies how the image should be laid out within its container. Possible values
183    /// include `Layout::Fill`, `Layout::Responsive`, `Layout::Intrinsic`, `Layout::Fixed`,
184    /// `Layout::Auto`, `Layout::Stretch`, and `Layout::ScaleDown`. Defaults to `Layout::Responsive`.
185    #[prop_or_default]
186    pub layout: Layout,
187
188    /// Reference to the DOM node.
189    ///
190    /// This is used to create a reference to the actual DOM element of the image. It is
191    /// useful for directly manipulating the image element via JavaScript if needed.
192    #[prop_or_default]
193    pub node_ref: NodeRef,
194
195    /// A list of one or more image sources for responsive loading.
196    ///
197    /// Defines multiple image resources for the browser to choose from, depending on screen size, resolution,
198    /// and other factors. Each source can include width (`w`) or pixel density (`x`) descriptors.
199    #[prop_or_default]
200    pub srcset: &'static str,
201
202    /// Cross-origin policy to use when fetching the image.
203    ///
204    /// Determines whether the image should be fetched with CORS enabled. Useful when the image needs to be accessed
205    /// in a `<canvas>` element. Accepts `anonymous` or `use-credentials`.
206    #[prop_or_default]
207    pub crossorigin: CrossOrigin,
208
209    /// Referrer policy to apply when fetching the image.
210    ///
211    /// Controls how much referrer information should be included with requests made for the image resource.
212    /// Common values include `no-referrer`, `origin`, `strict-origin-when-cross-origin`, etc.
213    #[prop_or_default]
214    pub referrerpolicy: ReferrerPolicy,
215
216    /// The fragment identifier of the image map to use.
217    ///
218    /// Associates the image with a `<map>` element, enabling clickable regions within the image. The value
219    /// should begin with `#` and match the `name` of the corresponding map element.
220    #[prop_or_default]
221    pub usemap: &'static str,
222
223    /// Indicates that the image is part of a server-side image map.
224    ///
225    /// When set, clicking the image will send the click coordinates to the server. Only allowed when the image
226    /// is inside an `<a>` element with a valid `href`.
227    #[prop_or_default]
228    pub ismap: bool,
229
230    /// Hints the browser about the priority of fetching this image.
231    ///
232    /// Helps the browser prioritize network resource loading. Accepts `high`, `low`, or `auto` (default).
233    /// See `HTMLImageElement.fetchPriority` for more.
234    #[prop_or_default]
235    pub fetchpriority: FetchPriority,
236
237    /// Identifier for tracking image performance timing.
238    ///
239    /// Registers the image with the `PerformanceElementTiming` API using the given string as its ID. Useful for
240    /// performance monitoring and analytics.
241    #[prop_or_default]
242    pub elementtiming: &'static str,
243
244    /// URL(s) to send Attribution Reporting requests for the image.
245    ///
246    /// Indicates that the browser should send an `Attribution-Reporting-Eligible` header with the image request.
247    /// Can be a boolean or a list of URLs for attribution registration on specified servers. Experimental feature.
248    #[prop_or_default]
249    pub attributionsrc: &'static str,
250
251    /// Indicates the current state of the image in a navigation menu.
252    ///
253    /// Valid values are "page", "step", "location", "date", "time", "true", "false".
254    /// This is useful for enhancing accessibility in navigation menus.
255    #[prop_or_default]
256    pub aria_current: &'static str,
257
258    /// Describes the image using the ID of the element that provides a description.
259    ///
260    /// The ID of the element that describes the image. This is used for accessibility
261    /// purposes, particularly for screen readers.
262    #[prop_or_default]
263    pub aria_describedby: &'static str,
264
265    /// Indicates whether the content associated with the image is currently expanded or collapsed.
266    ///
267    /// This is typically used for ARIA-based accessibility and is represented as "true", "false", or "undefined".
268    #[prop_or_default]
269    pub aria_expanded: &'static str,
270
271    /// Indicates whether the image is currently hidden from the user.
272    ///
273    /// This attribute is used for accessibility and indicates whether the image is visible
274    /// to the user or not. Valid values are "true", "false", or "undefined".
275    #[prop_or_default]
276    pub aria_hidden: &'static str,
277
278    /// Indicates whether the content associated with the image is live and dynamic.
279    ///
280    /// The value can be "off", "assertive", or "polite", helping assistive technologies
281    /// determine how to handle updates to the content.
282    #[prop_or_default]
283    pub aria_live: AriaLive,
284
285    /// Indicates whether the image is currently pressed or selected.
286    ///
287    /// This attribute can have values like "true", "false", "mixed", or "undefined".
288    #[prop_or_default]
289    pub aria_pressed: AriaPressed,
290
291    /// ID of the element that the image controls or owns.
292    ///
293    /// Specifies the ID of the element that the image controls or is associated with.
294    #[prop_or_default]
295    pub aria_controls: &'static str,
296
297    /// ID of the element that labels the image.
298    ///
299    /// Specifies the ID of the element that labels the image for accessibility purposes.
300    #[prop_or_default]
301    pub aria_labelledby: &'static str,
302}
303
304impl Default for ImageProps {
305    fn default() -> Self {
306        ImageProps {
307            src: "",
308            alt: "Image",
309            width: "",
310            height: "",
311            style: "",
312            class: "",
313            sizes: "",
314            quality: "",
315            placeholder: "empty",
316            on_load: Callback::noop(),
317            object_fit: ObjectFit::default(),
318            object_position: Position::default(),
319            on_error: Callback::noop(),
320            decoding: Decoding::default(),
321            blur_data_url: "",
322            lazy_boundary: "100px",
323            unoptimized: false,
324            layout: Layout::default(),
325            node_ref: NodeRef::default(),
326            fallback_src: "",
327            srcset: "",
328            crossorigin: CrossOrigin::default(),
329            loading: Loading::default(),
330            referrerpolicy: ReferrerPolicy::default(),
331            usemap: "",
332            ismap: false,
333            fetchpriority: FetchPriority::default(),
334            elementtiming: "",
335            attributionsrc: "",
336            aria_current: "",
337            aria_describedby: "",
338            aria_expanded: "",
339            aria_hidden: "",
340            aria_live: AriaLive::default(),
341            aria_pressed: AriaPressed::default(),
342            aria_controls: "",
343            aria_labelledby: "",
344        }
345    }
346}
347
348/// Image Component
349///
350/// A highly optimized and feature-rich `Image` component for Yew applications, supporting
351/// lazy loading, blur placeholders, fallback handling, and multiple responsive layouts.
352///
353/// # Properties
354/// The component uses the `ImageProps` struct for its properties. Key properties include:
355///
356/// - **src**: The main image source URL (`&'static str`). Required.
357/// - **alt**: Alternative text for accessibility (`&'static str`). Default: `""`.
358/// - **layout**: The image layout strategy (`Layout`). Default: `Layout::Auto`.
359/// - **width**: The width of the image (`&'static str`). Required for certain layouts.
360/// - **height**: The height of the image (`&'static str`). Required for certain layouts.
361/// - **sizes**: Defines responsive image sizes (`&'static str`). Default: `""`.
362/// - **quality**: Image quality (custom property) (`&'static str`). Optional.
363/// - **placeholder**: Placeholder strategy before the image loads (e.g., `"blur"`) (`&'static str`). Default: `""`.
364/// - **blur_data_url**: Base64-encoded low-res placeholder image (`&'static str`). Used when `placeholder` is `"blur"`.
365/// - **fallback_src**: Fallback image URL if the main `src` fails to load (`&'static str`). Optional.
366/// - **priority**: Whether to load the image eagerly instead of lazily (`bool`). Default: `false`.
367/// - **object_fit**: CSS `object-fit` value (`ObjectFit`). Default: `ObjectFit::Contain`.
368/// - **object_position**: Object positioning inside the container (`Position`). Default: `Position::Center`.
369/// - **style**: Additional inline CSS styles (`&'static str`). Default: `""`.
370/// - **class**: Additional CSS classes (`&'static str`). Default: `""`.
371/// - **decoding**: Decoding strategy (`Decoding`). Default: `Decoding::Auto`.
372/// - **on_load**: Callback invoked when the image successfully loads (`Callback<()>`). Default: no-op.
373/// - **on_error**: Callback invoked if loading or fallback loading fails (`Callback<String>`). Default: no-op.
374/// - **node_ref**: Node reference for the underlying `img` element (`NodeRef`).
375/// - **ARIA attributes**: Full ARIA support for better accessibility (`aria_label`, `aria_hidden`, etc.).
376///
377/// # Features
378/// - **Lazy Loading with IntersectionObserver**:
379///   Image is loaded **only when scrolled into view**, boosting performance and saving bandwidth.
380///
381/// - **Fallback Handling**:
382///   Automatically tries a `fallback_src` if the primary `src` fails, ensuring robustness.
383///
384/// - **Blur Placeholder**:
385///   Supports blurred low-resolution placeholders for smoother image transitions.
386///
387/// - **Multiple Layouts**:
388///   Supports various layouts like `Fill`, `Responsive`, `Intrinsic`, `Fixed`, `Auto`, `Stretch`, and `ScaleDown`.
389///
390/// - **Accessibility**:
391///   Built-in support for ARIA attributes to make images fully accessible.
392///
393/// # Layout Modes
394/// - `Layout::Fill`: Image stretches to fill its container absolutely.
395/// - `Layout::Responsive`: Maintains aspect ratio and scales responsively.
396/// - `Layout::Intrinsic`: Renders using natural image dimensions.
397/// - `Layout::Fixed`: Renders at a fixed size.
398/// - `Layout::Auto`: Default natural behavior without forcing constraints.
399/// - `Layout::Stretch`: Fills the parent container's width and height.
400/// - `Layout::ScaleDown`: Scales image down to fit the container without stretching.
401///
402/// # Examples
403///
404/// ## Basic Usage
405/// ```rust
406/// use yew::prelude::*;
407/// use image_rs::yew::Image;
408/// use image_rs::Layout;
409///
410/// #[function_component(App)]
411/// pub fn app() -> Html {
412///     html! {
413///         <Image
414///             src="/images/photo.jpg"
415///             alt="A beautiful view"
416///             width="800"
417///             height="600"
418///             layout={Layout::Responsive}
419///         />
420///     }
421/// }
422/// ```
423///
424/// ## Blur Placeholder
425/// ```rust
426/// use yew::prelude::*;
427/// use image_rs::yew::Image;
428/// use image_rs::Layout;
429///
430/// #[function_component(App)]
431/// pub fn app() -> Html {
432///     html! {
433///         <Image
434///             src="/images/photo.jpg"
435///             alt="Blur example"
436///             width="800"
437///             height="600"
438///             layout={Layout::Intrinsic}
439///             placeholder="blur"
440///             blur_data_url="data:image/jpeg;base64,..."
441///         />
442///     }
443/// }
444/// ```
445///
446/// ## Fallback Image
447/// ```rust
448/// use yew::prelude::*;
449/// use image_rs::yew::Image;
450/// use image_rs::Layout;
451///
452/// #[function_component(App)]
453/// pub fn app() -> Html {
454///     html! {
455///         <Image
456///             src="/images/main.jpg"
457///             fallback_src="/images/fallback.jpg"
458///             alt="With fallback"
459///             width="800"
460///             height="600"
461///             layout={Layout::Fixed}
462///         />
463///     }
464/// }
465/// ```
466///
467/// # Behavior
468/// - The image starts loading lazily once it enters the viewport (10% visible threshold).
469/// - If loading fails, a network fetch checks the fallback image.
470/// - Blur placeholder is rendered with a heavy `blur(20px)` effect until the full image loads.
471/// - Depending on the `Layout`, the container styling adjusts automatically.
472///
473/// # Notes
474/// - `width` and `height` are required for `Responsive`, `Intrinsic`, and `Fixed` layouts.
475/// - `Layout::Fill` ignores width and height, stretching to fit the parent container.
476/// - Accessibility attributes like `aria-label` and `aria-hidden` are passed directly to the `<img>` element.
477/// - Priority images (`priority = true`) are loaded eagerly instead of lazily.
478///
479/// # Errors
480/// - If both `src` and `fallback_src` fail, the `on_error` callback is triggered with an error message.
481/// - JSON parsing from a fallback response is attempted but not mandatory for image loading success.
482///
483/// # Optimization Techniques
484/// - **IntersectionObserver** is used for intelligent lazy loading.
485/// - **Caching** via `RequestCache::Reload` ensures fallback images are always fetched fresh if needed.
486/// - **Async/await** approach for fetch operations provides non-blocking fallback handling.
487///
488/// # See Also
489/// - [MDN img Element](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img)
490#[function_component]
491pub fn Image(props: &ImageProps) -> Html {
492    let mut props = props.clone();
493    let img_ref = props.node_ref.clone();
494
495    let img_ref_clone = img_ref.clone();
496    let on_load = props.on_load.clone();
497    let on_load_call = props.on_load.clone();
498
499    // Lazy Load Effect:
500    // Waits until the image **scrolls into view**, then dynamically **sets the src** to start loading it.
501    // Triggers an optional `on_load` callback once loading is initiated.
502    // Smart Optimization: Saves bandwidth and greatly improves page speed, especially for pages with **many images**!
503    // 9000 IQ Move: Only load images users actually *scroll to*, no more wasting bytes, gg!
504    use_effect_with(JsValue::from(props.src), move |_deps| {
505        let callback = Closure::wrap(Box::new(
506            move |entries: js_sys::Array, _observer: IntersectionObserver| {
507                if let Some(entry) = entries.get(0).dyn_ref::<IntersectionObserverEntry>() {
508                    if entry.is_intersecting() {
509                        if let Some(img) = img_ref_clone.cast::<web_sys::HtmlImageElement>() {
510                            img.set_src(props.src);
511                            on_load.emit(());
512                        }
513                    }
514                }
515            },
516        )
517            as Box<dyn FnMut(js_sys::Array, IntersectionObserver)>);
518
519        let options = IntersectionObserverInit::new();
520        // e.g., 10% visible
521        options.set_threshold(&js_sys::Array::of1(&0.1.into()));
522        // if Root is None, defaults to viewport
523
524        // Create observer
525        let observer =
526            IntersectionObserver::new_with_options(callback.as_ref().unchecked_ref(), &options)
527                .expect("Failed to create IntersectionObserver");
528
529        // Start observing
530        if let Some(img) = img_ref.cast::<web_sys::HtmlElement>() {
531            observer.observe(&img);
532        }
533
534        // Disconnect when unmount
535        let observer_clone = observer.clone();
536        let _cleanup = move || {
537            observer_clone.disconnect();
538        };
539
540        // Keep closure alive
541        callback.forget();
542    });
543
544    // This informs your app that the image failed to load and auto replace the image.
545    let fetch_data = {
546        Callback::from(move |_| {
547            let loading_complete_callback = props.on_load.clone();
548            let on_error_callback = props.on_error.clone();
549            spawn_local(async move {
550                match Request::get(props.fallback_src)
551                    .cache(RequestCache::Reload)
552                    .send()
553                    .await
554                {
555                    Ok(response) => {
556                        if response.status() == 200 {
557                            let json_result = response.json::<serde_json::Value>();
558                            match json_result.await {
559                                Ok(_data) => {
560                                    props.src = props.fallback_src;
561                                    loading_complete_callback.emit(());
562                                }
563                                Err(_err) => {
564                                    on_error_callback.emit("Image Not Found!".to_string());
565                                }
566                            }
567                        } else {
568                            let status = response.status();
569                            let body = response.text().await.unwrap_or_else(|_| {
570                                String::from("Failed to retrieve response body")
571                            });
572                            on_error_callback.emit(format!(
573                                "Failed to load image. Status: {}, Body: {:?}",
574                                status, body
575                            ));
576                        }
577                    }
578
579                    Err(err) => {
580                        // Handle network errors
581                        on_error_callback.emit(format!("Network error: {}", err));
582                    }
583                }
584            });
585        })
586    };
587
588    let img_style = {
589        let mut style = String::new();
590        style.push_str(&format!("object-fit: {};", props.object_fit.as_str()));
591        style.push_str(&format!(
592            "object-position: {};",
593            props.object_position.as_str()
594        ));
595        if !props.style.is_empty() {
596            style.push_str(props.style);
597        }
598        style
599    };
600
601    let blur_style = if props.placeholder == "blur" {
602        format!(
603            "background-size: {}; background-position: {}; filter: blur(20px); background-image: url(\"{}\")",
604            props.sizes,
605            props.object_position.as_str(),
606            props.blur_data_url
607        )
608    } else {
609        String::new()
610    };
611
612    let onload = {
613        Callback::from(move |_| {
614            on_load_call.emit(());
615        })
616    };
617
618    let layout = match props.layout {
619        Layout::Fill => {
620            html! {
621                <span style={"display: block; position: absolute; top: 0; left: 0; bottom: 0; right: 0;"}>
622                    <img
623                        src={props.src}
624                        alt={props.alt}
625                        width={props.width}
626                        height={props.height}
627                        style={img_style}
628                        class={props.class}
629                        loading={props.loading.as_str()}
630                        sizes={props.sizes}
631                        quality={props.quality}
632                        placeholder={props.placeholder}
633                        decoding={props.decoding.as_str()}
634                        ref={props.node_ref}
635                        role="img"
636                        aria-label={props.alt}
637                        aria-labelledby={props.aria_labelledby}
638                        aria-describedby={props.aria_describedby}
639                        aria-hidden={props.aria_hidden}
640                        aria-current={props.aria_current}
641                        aria-expanded={props.aria_expanded}
642                        aria-live={props.aria_live.as_str()}
643                        aria-pressed={props.aria_pressed.as_str()}
644                        aria-controls={props.aria_controls}
645                        onerror={fetch_data}
646                        style={blur_style}
647                        crossorigin={props.crossorigin.as_str()}
648                        referrerpolicy={props.referrerpolicy.as_str()}
649                        fetchpriority={props.fetchpriority.as_str()}
650                        attributionsrc={props.attributionsrc}
651                        onload={onload}
652                        elementtiming={props.elementtiming}
653                        srcset={props.srcset}
654                        ismap={props.ismap}
655                        usemap={props.usemap}
656                    />
657                </span>
658            }
659        }
660        Layout::Responsive => {
661            let quotient: f64 =
662                props.height.parse::<f64>().unwrap() / props.width.parse::<f64>().unwrap();
663            let padding_top: String = if quotient.is_nan() {
664                "100%".to_string()
665            } else {
666                format!("{}%", quotient * 100.0)
667            };
668
669            html! {
670                <span style={"display: block; position: relative;"}>
671                    <span style={"padding-top: ".to_owned() + &padding_top}>
672                        <img
673                            src={props.src}
674                            alt={props.alt}
675                            width={props.width}
676                            height={props.height}
677                            style={img_style}
678                            class={props.class}
679                            sizes={props.sizes}
680                            quality={props.quality}
681                            loading={props.loading.as_str()}
682                            placeholder={props.placeholder}
683                            decoding={props.decoding.as_str()}
684                            ref={props.node_ref}
685                            role="img"
686                            aria-label={props.alt}
687                            aria-labelledby={props.aria_labelledby}
688                            aria-describedby={props.aria_describedby}
689                            aria-hidden={props.aria_hidden}
690                            aria-current={props.aria_current}
691                            aria-expanded={props.aria_expanded}
692                            aria-live={props.aria_live.as_str()}
693                            aria-pressed={props.aria_pressed.as_str()}
694                            aria-controls={props.aria_controls}
695                            onerror={fetch_data}
696                            style={blur_style}
697                            crossorigin={props.crossorigin.as_str()}
698                            referrerpolicy={props.referrerpolicy.as_str()}
699                            fetchpriority={props.fetchpriority.as_str()}
700                            attributionsrc={props.attributionsrc}
701                            onload={onload}
702                            elementtiming={props.elementtiming}
703                            srcset={props.srcset}
704                            ismap={props.ismap}
705                            usemap={props.usemap}
706                        />
707                    </span>
708                </span>
709            }
710        }
711        Layout::Intrinsic => {
712            html! {
713                <span style={"display: inline-block; position: relative; max-width: 100%;"}>
714                    <span style={"max-width: 100%;"}>
715                        <img
716                            src={props.src}
717                            alt={props.alt}
718                            width={props.width}
719                            height={props.height}
720                            style={img_style}
721                            class={props.class}
722                            sizes={props.sizes}
723                            loading={props.loading.as_str()}
724                            quality={props.quality}
725                            placeholder={props.placeholder}
726                            decoding={props.decoding.as_str()}
727                            ref={props.node_ref}
728                            role="img"
729                            aria-label={props.alt}
730                            aria-labelledby={props.aria_labelledby}
731                            aria-describedby={props.aria_describedby}
732                            aria-hidden={props.aria_hidden}
733                            aria-current={props.aria_current}
734                            aria-expanded={props.aria_expanded}
735                            aria-live={props.aria_live.as_str()}
736                            aria-pressed={props.aria_pressed.as_str()}
737                            aria-controls={props.aria_controls}
738                            onerror={fetch_data}
739                            style={blur_style}
740                            crossorigin={props.crossorigin.as_str()}
741                            referrerpolicy={props.referrerpolicy.as_str()}
742                            fetchpriority={props.fetchpriority.as_str()}
743                            attributionsrc={props.attributionsrc}
744                            onload={onload}
745                            elementtiming={props.elementtiming}
746                            srcset={props.srcset}
747                            ismap={props.ismap}
748                            usemap={props.usemap}
749                        />
750                    </span>
751                    <img
752                        src={props.blur_data_url}
753                        style={"display: none;"}
754                        alt={props.alt}
755                        aria-hidden="true"
756                    />
757                </span>
758            }
759        }
760        Layout::Fixed => {
761            html! {
762                <span style={"display: inline-block; position: relative;"}>
763                    <img
764                        src={props.src}
765                        alt={props.alt}
766                        width={props.width}
767                        height={props.height}
768                        style={img_style}
769                        class={props.class}
770                        sizes={props.sizes}
771                        quality={props.quality}
772                        loading={props.loading.as_str()}
773                        placeholder={props.placeholder}
774                        decoding={props.decoding.as_str()}
775                        ref={props.node_ref}
776                        role="img"
777                        aria-label={props.alt}
778                        aria-labelledby={props.aria_labelledby}
779                        aria-describedby={props.aria_describedby}
780                        aria-hidden={props.aria_hidden}
781                        aria-current={props.aria_current}
782                        aria-expanded={props.aria_expanded}
783                        aria-live={props.aria_live.as_str()}
784                        aria-pressed={props.aria_pressed.as_str()}
785                        aria-controls={props.aria_controls}
786                        onerror={fetch_data}
787                        style={blur_style}
788                        crossorigin={props.crossorigin.as_str()}
789                        referrerpolicy={props.referrerpolicy.as_str()}
790                        fetchpriority={props.fetchpriority.as_str()}
791                        attributionsrc={props.attributionsrc}
792                        onload={onload}
793                        elementtiming={props.elementtiming}
794                        srcset={props.srcset}
795                        ismap={props.ismap}
796                        usemap={props.usemap}
797                    />
798                </span>
799            }
800        }
801        Layout::Auto => {
802            // Preserve the natural size of the image
803            html! {
804                <span style={"display: inline-block; position: relative;"}>
805                    <img
806                        src={props.src}
807                        alt={props.alt}
808                        width={props.width}
809                        height={props.height}
810                        style={img_style}
811                        class={props.class}
812                        sizes={props.sizes}
813                        quality={props.quality}
814                        placeholder={props.placeholder}
815                        loading={props.loading.as_str()}
816                        decoding={props.decoding.as_str()}
817                        ref={props.node_ref}
818                        role="img"
819                        aria-label={props.alt}
820                        aria-labelledby={props.aria_labelledby}
821                        aria-describedby={props.aria_describedby}
822                        aria-hidden={props.aria_hidden}
823                        aria-current={props.aria_current}
824                        aria-expanded={props.aria_expanded}
825                        aria-live={props.aria_live.as_str()}
826                        aria-pressed={props.aria_pressed.as_str()}
827                        aria-controls={props.aria_controls}
828                        onerror={fetch_data}
829                        style={blur_style}
830                        crossorigin={props.crossorigin.as_str()}
831                        referrerpolicy={props.referrerpolicy.as_str()}
832                        fetchpriority={props.fetchpriority.as_str()}
833                        attributionsrc={props.attributionsrc}
834                        onload={onload}
835                        elementtiming={props.elementtiming}
836                        srcset={props.srcset}
837                        ismap={props.ismap}
838                        usemap={props.usemap}
839                    />
840                </span>
841            }
842        }
843        Layout::Stretch => {
844            // Make the image fill the container
845            html! {
846                <span style={"display: block; width: 100%; height: 100%; position: relative;"}>
847                    <img
848                        src={props.src}
849                        alt={props.alt}
850                        width="100%"
851                        height="100%"
852                        style={img_style}
853                        class={props.class}
854                        loading={props.loading.as_str()}
855                        sizes={props.sizes}
856                        quality={props.quality}
857                        placeholder={props.placeholder}
858                        decoding={props.decoding.as_str()}
859                        ref={props.node_ref}
860                        role="img"
861                        aria-label={props.alt}
862                        aria-labelledby={props.aria_labelledby}
863                        aria-describedby={props.aria_describedby}
864                        aria-hidden={props.aria_hidden}
865                        aria-current={props.aria_current}
866                        aria-expanded={props.aria_expanded}
867                        aria-live={props.aria_live.as_str()}
868                        aria-pressed={props.aria_pressed.as_str()}
869                        aria-controls={props.aria_controls}
870                        onerror={fetch_data}
871                        style={blur_style}
872                        crossorigin={props.crossorigin.as_str()}
873                        referrerpolicy={props.referrerpolicy.as_str()}
874                        fetchpriority={props.fetchpriority.as_str()}
875                        attributionsrc={props.attributionsrc}
876                        onload={onload}
877                        elementtiming={props.elementtiming}
878                        srcset={props.srcset}
879                        ismap={props.ismap}
880                        usemap={props.usemap}
881                    />
882                </span>
883            }
884        }
885        Layout::ScaleDown => {
886            // Maintain aspect ratio, but scale down if image is too large
887            html! {
888                <span style={"display: inline-block; position: relative; max-width: 100%; max-height: 100%;"}>
889                    <img
890                        src={props.src}
891                        alt={props.alt}
892                        width={props.width}
893                        height={props.height}
894                        style={img_style}
895                        class={props.class}
896                        loading={props.loading.as_str()}
897                        sizes={props.sizes}
898                        quality={props.quality}
899                        placeholder={props.placeholder}
900                        decoding={props.decoding.as_str()}
901                        ref={props.node_ref}
902                        role="img"
903                        aria-label={props.alt}
904                        aria-labelledby={props.aria_labelledby}
905                        aria-describedby={props.aria_describedby}
906                        aria-hidden={props.aria_hidden}
907                        aria-current={props.aria_current}
908                        aria-expanded={props.aria_expanded}
909                        aria-live={props.aria_live.as_str()}
910                        aria-pressed={props.aria_pressed.as_str()}
911                        aria-controls={props.aria_controls}
912                        onerror={fetch_data}
913                        style={blur_style}
914                        crossorigin={props.crossorigin.as_str()}
915                        referrerpolicy={props.referrerpolicy.as_str()}
916                        fetchpriority={props.fetchpriority.as_str()}
917                        attributionsrc={props.attributionsrc}
918                        onload={onload}
919                        elementtiming={props.elementtiming}
920                        srcset={props.srcset}
921                        ismap={props.ismap}
922                        usemap={props.usemap}
923                    />
924                </span>
925            }
926        }
927    };
928    html! {
929            {layout}
930    }
931}