Spaces:
				
			
			
	
			
			
					
		Running
		
	
	
	
			
			
	
	
	
	
		
		
					
		Running
		
	Upload folder using huggingface_hub
Browse files- .gitattributes +1 -0
- .gitignore +2 -1
- README.md +62 -33
- app.py +232 -231
- space.py +50 -32
- src/.gitignore +2 -1
- src/README.md +62 -33
- src/backend/gradio_imagemeta/helpers.py +1 -0
- src/backend/gradio_imagemeta/templates/component/index.js +0 -0
- src/backend/gradio_imagemeta/templates/component/style.css +1 -1
- src/demo/app.py +51 -5
- src/demo/space.py +50 -32
- src/frontend/shared/ImagePreview.svelte +1 -1
- src/frontend/shared/ImageUploader.svelte +1 -1
- src/outputs/image_with_meta.png +3 -0
- src/pyproject.toml +1 -1
    	
        .gitattributes
    CHANGED
    
    | @@ -36,3 +36,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text | |
| 36 | 
             
            src/.gradio/cached_examples/24/Upload[[:space:]]Image[[:space:]]All[[:space:]]metadata/7a159874ef63b94d8fa4/image_with_meta.png filter=lfs diff=lfs merge=lfs -text
         | 
| 37 | 
             
            src/.gradio/cached_examples/24/Upload[[:space:]]Image[[:space:]]Custom[[:space:]]metadata[[:space:]]only/8a0de8f0e921a79b67a8/image_with_meta.png filter=lfs diff=lfs merge=lfs -text
         | 
| 38 | 
             
            src/examples/image_with_meta.png filter=lfs diff=lfs merge=lfs -text
         | 
|  | 
|  | |
| 36 | 
             
            src/.gradio/cached_examples/24/Upload[[:space:]]Image[[:space:]]All[[:space:]]metadata/7a159874ef63b94d8fa4/image_with_meta.png filter=lfs diff=lfs merge=lfs -text
         | 
| 37 | 
             
            src/.gradio/cached_examples/24/Upload[[:space:]]Image[[:space:]]Custom[[:space:]]metadata[[:space:]]only/8a0de8f0e921a79b67a8/image_with_meta.png filter=lfs diff=lfs merge=lfs -text
         | 
| 38 | 
             
            src/examples/image_with_meta.png filter=lfs diff=lfs merge=lfs -text
         | 
| 39 | 
            +
            src/outputs/image_with_meta.png filter=lfs diff=lfs merge=lfs -text
         | 
    	
        .gitignore
    CHANGED
    
    | @@ -12,4 +12,5 @@ __tmp/* | |
| 12 | 
             
            .ruff_cache
         | 
| 13 | 
             
            node_modules
         | 
| 14 | 
             
            backend/**/templates/
         | 
| 15 | 
            -
            outputs/
         | 
|  | 
|  | |
| 12 | 
             
            .ruff_cache
         | 
| 13 | 
             
            node_modules
         | 
| 14 | 
             
            backend/**/templates/
         | 
| 15 | 
            +
            outputs/
         | 
| 16 | 
            +
            README_TEMPLATE.md
         | 
    	
        README.md
    CHANGED
    
    | @@ -10,12 +10,23 @@ app_file: space.py | |
| 10 | 
             
            ---
         | 
| 11 |  | 
| 12 | 
             
            # `gradio_imagemeta`
         | 
| 13 | 
            -
            <img alt="Static Badge" src="https://img.shields.io/badge/version%20-%200.0.1%20-% | 
| 14 |  | 
| 15 | 
             
            Image Preview with Metadata for Gradio Interface
         | 
| 16 |  | 
| 17 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 18 |  | 
|  | |
| 19 | 
             
            ```bash
         | 
| 20 | 
             
            pip install gradio_imagemeta
         | 
| 21 | 
             
            ```
         | 
| @@ -32,6 +43,7 @@ from gradio_propertysheet import PropertySheet | |
| 32 | 
             
            from gradio_propertysheet.helpers import build_dataclass_fields, create_dataclass_instance
         | 
| 33 | 
             
            from pathlib import Path
         | 
| 34 |  | 
|  | |
| 35 | 
             
            output_dir = Path("outputs")
         | 
| 36 | 
             
            output_dir.mkdir(exist_ok=True)
         | 
| 37 |  | 
| @@ -52,25 +64,50 @@ class PropertyConfig: | |
| 52 | 
             
                image_settings: ImageSettings = field(default_factory=ImageSettings)
         | 
| 53 | 
             
                description: str = field(default="", metadata={"label": "Description"})
         | 
| 54 |  | 
| 55 | 
            -
            def  | 
| 56 | 
             
                """
         | 
| 57 | 
            -
                 | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 58 |  | 
| 59 | 
             
                Args:
         | 
| 60 | 
            -
                     | 
| 61 | 
            -
                    img_all_path: File path for the image to display in img_all.
         | 
| 62 |  | 
| 63 | 
             
                Returns:
         | 
| 64 | 
            -
                     | 
| 65 | 
             
                """
         | 
| 66 | 
            -
                 | 
| 67 | 
            -
             | 
| 68 | 
            -
                     | 
| 69 | 
            -
             | 
| 70 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 71 |  | 
| 72 | 
            -
                #  | 
| 73 | 
            -
                 | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 74 |  | 
| 75 | 
             
            def handle_load_metadata(image_data: ImageMeta | None) -> List[Any]:
         | 
| 76 | 
             
                """
         | 
| @@ -90,11 +127,11 @@ def handle_load_metadata(image_data: ImageMeta | None) -> List[Any]: | |
| 90 | 
             
                raw_values = transfer_metadata(output_fields, metadata, dataclass_fields)
         | 
| 91 |  | 
| 92 | 
             
                output_values = [gr.skip()] * len(output_fields)
         | 
| 93 | 
            -
                for i, (component, value) in enumerate(zip(output_fields, raw_values)):
         | 
| 94 | 
             
                    if hasattr(component, 'root_label'):
         | 
| 95 | 
             
                        output_values[i] = create_dataclass_instance(PropertyConfig, value)
         | 
| 96 | 
             
                    else:
         | 
| 97 | 
            -
                        output_values[i] = gr. | 
| 98 |  | 
| 99 | 
             
                return output_values
         | 
| 100 |  | 
| @@ -114,10 +151,11 @@ def save_image_with_metadata(image_data: Any, *inputs: Any) -> str | None: | |
| 114 |  | 
| 115 | 
             
                params = list(inputs)
         | 
| 116 | 
             
                image_params = dict(zip(input_fields.keys(), params))
         | 
| 117 | 
            -
                dataclass_fields = build_dataclass_fields(PropertyConfig)
         | 
| 118 | 
            -
                metadata = {label: image_params.get(label, "") for label in  | 
| 119 |  | 
| 120 | 
             
                new_filepath = output_dir / "image_with_meta.png"
         | 
|  | |
| 121 | 
             
                add_metadata(image_data, metadata, new_filepath)
         | 
| 122 |  | 
| 123 | 
             
                return str(new_filepath)
         | 
| @@ -142,17 +180,18 @@ with gr.Blocks() as demo: | |
| 142 | 
             
                        label="Upload Image (Custom metadata only)",
         | 
| 143 | 
             
                        type="filepath",
         | 
| 144 | 
             
                        width=300,
         | 
| 145 | 
            -
                        height=400,
         | 
| 146 | 
            -
                        disable_preprocess=False,
         | 
| 147 | 
             
                        interactive=True
         | 
| 148 | 
             
                    )
         | 
| 149 | 
             
                    img_all = ImageMeta(
         | 
| 150 | 
             
                        label="Upload Image (All metadata)",
         | 
| 151 | 
             
                        only_custom_metadata=False,
         | 
|  | |
| 152 | 
             
                        width=300,
         | 
| 153 | 
            -
                        height=400,
         | 
| 154 | 
             
                        popup_metadata_height=400,
         | 
| 155 | 
            -
                        popup_metadata_width=500
         | 
|  | |
| 156 | 
             
                    )
         | 
| 157 |  | 
| 158 | 
             
                gr.Markdown("## Metadata Viewer")
         | 
| @@ -178,17 +217,7 @@ with gr.Blocks() as demo: | |
| 178 | 
             
                with gr.Row():
         | 
| 179 | 
             
                    save_button = gr.Button("Add Metadata and Save Image")
         | 
| 180 | 
             
                    saved_file_output = gr.File(label="Download Image")
         | 
| 181 | 
            -
             | 
| 182 | 
            -
                with gr.Row():
         | 
| 183 | 
            -
                    gr.Examples(
         | 
| 184 | 
            -
                        examples=[
         | 
| 185 | 
            -
                            ["./examples/image_with_meta.png", "./examples/image_with_meta.png"]
         | 
| 186 | 
            -
                        ],
         | 
| 187 | 
            -
                        fn=process_example_images,
         | 
| 188 | 
            -
                        inputs=[img_custom, img_all],
         | 
| 189 | 
            -
                        outputs=[img_custom, img_all],
         | 
| 190 | 
            -
                        cache_examples=True
         | 
| 191 | 
            -
                    )
         | 
| 192 |  | 
| 193 | 
             
                input_fields = {
         | 
| 194 | 
             
                    "Model": model_box,
         | 
|  | |
| 10 | 
             
            ---
         | 
| 11 |  | 
| 12 | 
             
            # `gradio_imagemeta`
         | 
| 13 | 
            +
            <img alt="Static Badge" src="https://img.shields.io/badge/version%20-%200.0.1%20-%20blue"> <a href="https://huggingface.co/spaces/elismasilva/gradio_imagemeta"><img src="https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Demo-blue"></a><p><span>💻 <a href='https://github.com/DEVAIEXP/gradio_component_imagemeta'>Component GitHub Code</a></span></p>
         | 
| 14 |  | 
| 15 | 
             
            Image Preview with Metadata for Gradio Interface
         | 
| 16 |  | 
| 17 | 
            +
            Imagine loading a photo with embedded presets (e.g., camera settings or AI model parameters) and instantly populating your app’s UI with those values. With ImageMeta, you can extract and apply these presets effortlessly, streamlining workflows for photographers, data scientists, or creative professionals.
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            # Features and Key Characteristics
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            **ImageMeta** is a custom Gradio component designed to enhance image handling with robust metadata support. Key features include:
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            - **Interactive Image Upload**: Upload PNG/JPEG images via drag-and-drop or file selection, with real-time metadata extraction using `exifr` on the client side.
         | 
| 24 | 
            +
            - **Metadata Extraction & Display**: View EXIF, IPTC, and XMP metadata in a customizable popup, with options to filter custom metadata (e.g., `Model`, `Schurn`) or include technical details (e.g., `ImageWidth`).
         | 
| 25 | 
            +
            - **Preset Loading**: Load metadata directly into UI components (Textbox, Slider, PropertySheet) to apply saved presets, streamlining workflows like camera settings or AI model parameters.
         | 
| 26 | 
            +
            - **Metadata Editing & Saving**: Add or update metadata and save images with embedded metadata for downstream use, powered by PIL on the server side.
         | 
| 27 | 
            +
            - **Responsive Design**: Supports fullscreen mode, adjustable popup sizes, and a polished Svelte-based UI for seamless user experiences.
         | 
| 28 |  | 
| 29 | 
            +
            ## Installation
         | 
| 30 | 
             
            ```bash
         | 
| 31 | 
             
            pip install gradio_imagemeta
         | 
| 32 | 
             
            ```
         | 
|  | |
| 43 | 
             
            from gradio_propertysheet.helpers import build_dataclass_fields, create_dataclass_instance
         | 
| 44 | 
             
            from pathlib import Path
         | 
| 45 |  | 
| 46 | 
            +
             | 
| 47 | 
             
            output_dir = Path("outputs")
         | 
| 48 | 
             
            output_dir.mkdir(exist_ok=True)
         | 
| 49 |  | 
|  | |
| 64 | 
             
                image_settings: ImageSettings = field(default_factory=ImageSettings)
         | 
| 65 | 
             
                description: str = field(default="", metadata={"label": "Description"})
         | 
| 66 |  | 
| 67 | 
            +
            def infer_type(s: str):
         | 
| 68 | 
             
                """
         | 
| 69 | 
            +
                Infers and converts a string to the most likely data type.
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                It attempts conversions in the following order:
         | 
| 72 | 
            +
                1. Integer
         | 
| 73 | 
            +
                2. Float
         | 
| 74 | 
            +
                3. Boolean (case-insensitive 'true' or 'false')
         | 
| 75 | 
            +
                If all conversions fail, it returns the original string.
         | 
| 76 |  | 
| 77 | 
             
                Args:
         | 
| 78 | 
            +
                    s: The input string to be converted.
         | 
|  | |
| 79 |  | 
| 80 | 
             
                Returns:
         | 
| 81 | 
            +
                    The converted value (int, float, bool) or the original string.
         | 
| 82 | 
             
                """
         | 
| 83 | 
            +
                if not isinstance(s, str):
         | 
| 84 | 
            +
                    # If the input is not a string, return it as is.
         | 
| 85 | 
            +
                    return s
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                # 1. Try to convert to an integer
         | 
| 88 | 
            +
                try:
         | 
| 89 | 
            +
                    return int(s)
         | 
| 90 | 
            +
                except ValueError:
         | 
| 91 | 
            +
                    # Not an integer, continue...
         | 
| 92 | 
            +
                    pass
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                # 2. Try to convert to a float
         | 
| 95 | 
            +
                try:
         | 
| 96 | 
            +
                    return float(s)
         | 
| 97 | 
            +
                except ValueError:
         | 
| 98 | 
            +
                    # Not a float, continue...
         | 
| 99 | 
            +
                    pass
         | 
| 100 |  | 
| 101 | 
            +
                # 3. Check for a boolean value
         | 
| 102 | 
            +
                # This explicit check is important because bool('False') evaluates to True.
         | 
| 103 | 
            +
                s_lower = s.lower()
         | 
| 104 | 
            +
                if s_lower == 'true':
         | 
| 105 | 
            +
                    return True
         | 
| 106 | 
            +
                if s_lower == 'false':
         | 
| 107 | 
            +
                    return False
         | 
| 108 | 
            +
                    
         | 
| 109 | 
            +
                # 4. If nothing else worked, return the original string
         | 
| 110 | 
            +
                return s
         | 
| 111 |  | 
| 112 | 
             
            def handle_load_metadata(image_data: ImageMeta | None) -> List[Any]:
         | 
| 113 | 
             
                """
         | 
|  | |
| 127 | 
             
                raw_values = transfer_metadata(output_fields, metadata, dataclass_fields)
         | 
| 128 |  | 
| 129 | 
             
                output_values = [gr.skip()] * len(output_fields)
         | 
| 130 | 
            +
                for i, (component, value) in enumerate(zip(output_fields, raw_values)):        
         | 
| 131 | 
             
                    if hasattr(component, 'root_label'):
         | 
| 132 | 
             
                        output_values[i] = create_dataclass_instance(PropertyConfig, value)
         | 
| 133 | 
             
                    else:
         | 
| 134 | 
            +
                        output_values[i] = gr.update(value=infer_type(value))
         | 
| 135 |  | 
| 136 | 
             
                return output_values
         | 
| 137 |  | 
|  | |
| 151 |  | 
| 152 | 
             
                params = list(inputs)
         | 
| 153 | 
             
                image_params = dict(zip(input_fields.keys(), params))
         | 
| 154 | 
            +
                #dataclass_fields = build_dataclass_fields(PropertyConfig)
         | 
| 155 | 
            +
                metadata = {label: image_params.get(label, "") for label in image_params.keys()}
         | 
| 156 |  | 
| 157 | 
             
                new_filepath = output_dir / "image_with_meta.png"
         | 
| 158 | 
            +
                
         | 
| 159 | 
             
                add_metadata(image_data, metadata, new_filepath)
         | 
| 160 |  | 
| 161 | 
             
                return str(new_filepath)
         | 
|  | |
| 180 | 
             
                        label="Upload Image (Custom metadata only)",
         | 
| 181 | 
             
                        type="filepath",
         | 
| 182 | 
             
                        width=300,
         | 
| 183 | 
            +
                        height=400,            
         | 
|  | |
| 184 | 
             
                        interactive=True
         | 
| 185 | 
             
                    )
         | 
| 186 | 
             
                    img_all = ImageMeta(
         | 
| 187 | 
             
                        label="Upload Image (All metadata)",
         | 
| 188 | 
             
                        only_custom_metadata=False,
         | 
| 189 | 
            +
                        type="filepath",
         | 
| 190 | 
             
                        width=300,
         | 
| 191 | 
            +
                        height=400,            
         | 
| 192 | 
             
                        popup_metadata_height=400,
         | 
| 193 | 
            +
                        popup_metadata_width=500,
         | 
| 194 | 
            +
                        interactive=True
         | 
| 195 | 
             
                    )
         | 
| 196 |  | 
| 197 | 
             
                gr.Markdown("## Metadata Viewer")
         | 
|  | |
| 217 | 
             
                with gr.Row():
         | 
| 218 | 
             
                    save_button = gr.Button("Add Metadata and Save Image")
         | 
| 219 | 
             
                    saved_file_output = gr.File(label="Download Image")
         | 
| 220 | 
            +
               
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 221 |  | 
| 222 | 
             
                input_fields = {
         | 
| 223 | 
             
                    "Model": model_box,
         | 
    	
        app.py
    CHANGED
    
    | @@ -1,231 +1,232 @@ | |
| 1 | 
            -
            from dataclasses import dataclass, field
         | 
| 2 | 
            -
            from typing import List, Any
         | 
| 3 | 
            -
            import gradio as gr
         | 
| 4 | 
            -
            from gradio_imagemeta import ImageMeta
         | 
| 5 | 
            -
            from gradio_imagemeta.helpers import extract_metadata, add_metadata, transfer_metadata
         | 
| 6 | 
            -
            from gradio_propertysheet import PropertySheet
         | 
| 7 | 
            -
            from gradio_propertysheet.helpers import build_dataclass_fields, create_dataclass_instance
         | 
| 8 | 
            -
            from pathlib import Path
         | 
| 9 | 
            -
             | 
| 10 | 
            -
             | 
| 11 | 
            -
            output_dir | 
| 12 | 
            -
             | 
| 13 | 
            -
             | 
| 14 | 
            -
             | 
| 15 | 
            -
             | 
| 16 | 
            -
                 | 
| 17 | 
            -
                 | 
| 18 | 
            -
                 | 
| 19 | 
            -
                 | 
| 20 | 
            -
             | 
| 21 | 
            -
                     | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 27 | 
            -
                 | 
| 28 | 
            -
                 | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 31 | 
            -
             | 
| 32 | 
            -
                 | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 35 | 
            -
                 | 
| 36 | 
            -
                 | 
| 37 | 
            -
                 | 
| 38 | 
            -
                 | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 45 | 
            -
             | 
| 46 | 
            -
                 | 
| 47 | 
            -
             | 
| 48 | 
            -
                    return  | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
                 | 
| 52 | 
            -
             | 
| 53 | 
            -
             | 
| 54 | 
            -
             | 
| 55 | 
            -
                     | 
| 56 | 
            -
             | 
| 57 | 
            -
             | 
| 58 | 
            -
                 | 
| 59 | 
            -
             | 
| 60 | 
            -
             | 
| 61 | 
            -
             | 
| 62 | 
            -
                     | 
| 63 | 
            -
             | 
| 64 | 
            -
                 | 
| 65 | 
            -
                #  | 
| 66 | 
            -
                 | 
| 67 | 
            -
                 | 
| 68 | 
            -
             | 
| 69 | 
            -
             | 
| 70 | 
            -
             | 
| 71 | 
            -
                    
         | 
| 72 | 
            -
             | 
| 73 | 
            -
                return  | 
| 74 | 
            -
             | 
| 75 | 
            -
             | 
| 76 | 
            -
             | 
| 77 | 
            -
                 | 
| 78 | 
            -
             | 
| 79 | 
            -
             | 
| 80 | 
            -
             | 
| 81 | 
            -
             | 
| 82 | 
            -
             | 
| 83 | 
            -
             | 
| 84 | 
            -
             | 
| 85 | 
            -
                 | 
| 86 | 
            -
             | 
| 87 | 
            -
             | 
| 88 | 
            -
             | 
| 89 | 
            -
                 | 
| 90 | 
            -
                 | 
| 91 | 
            -
             | 
| 92 | 
            -
             | 
| 93 | 
            -
                 | 
| 94 | 
            -
             | 
| 95 | 
            -
             | 
| 96 | 
            -
             | 
| 97 | 
            -
             | 
| 98 | 
            -
             | 
| 99 | 
            -
                 | 
| 100 | 
            -
                
         | 
| 101 | 
            -
             | 
| 102 | 
            -
             | 
| 103 | 
            -
                 | 
| 104 | 
            -
             | 
| 105 | 
            -
             | 
| 106 | 
            -
             | 
| 107 | 
            -
                     | 
| 108 | 
            -
             | 
| 109 | 
            -
             | 
| 110 | 
            -
             | 
| 111 | 
            -
             | 
| 112 | 
            -
                 | 
| 113 | 
            -
             | 
| 114 | 
            -
             | 
| 115 | 
            -
                 | 
| 116 | 
            -
                 | 
| 117 | 
            -
                 | 
| 118 | 
            -
                metadata = {label: image_params.get(label, "") for label in image_params.keys()}
         | 
| 119 | 
            -
                
         | 
| 120 | 
            -
                new_filepath = output_dir / "image_with_meta.png" | 
| 121 | 
            -
                 | 
| 122 | 
            -
                
         | 
| 123 | 
            -
                 | 
| 124 | 
            -
             | 
| 125 | 
            -
             | 
| 126 | 
            -
             | 
| 127 | 
            -
             | 
| 128 | 
            -
             | 
| 129 | 
            -
                gr.Markdown(
         | 
| 130 | 
            -
             | 
| 131 | 
            -
                     | 
| 132 | 
            -
                     | 
| 133 | 
            -
                     | 
| 134 | 
            -
                     | 
| 135 | 
            -
                     | 
| 136 | 
            -
                     | 
| 137 | 
            -
                    """
         | 
| 138 | 
            -
             | 
| 139 | 
            -
                 | 
| 140 | 
            -
                 | 
| 141 | 
            -
             | 
| 142 | 
            -
             | 
| 143 | 
            -
                         | 
| 144 | 
            -
                         | 
| 145 | 
            -
                         | 
| 146 | 
            -
                         | 
| 147 | 
            -
             | 
| 148 | 
            -
                     | 
| 149 | 
            -
             | 
| 150 | 
            -
                         | 
| 151 | 
            -
                         | 
| 152 | 
            -
                         | 
| 153 | 
            -
                         | 
| 154 | 
            -
                         | 
| 155 | 
            -
                         | 
| 156 | 
            -
                         | 
| 157 | 
            -
             | 
| 158 | 
            -
             | 
| 159 | 
            -
             | 
| 160 | 
            -
                gr.Markdown(" | 
| 161 | 
            -
                 | 
| 162 | 
            -
             | 
| 163 | 
            -
                     | 
| 164 | 
            -
                     | 
| 165 | 
            -
                     | 
| 166 | 
            -
                     | 
| 167 | 
            -
             | 
| 168 | 
            -
                 | 
| 169 | 
            -
                 | 
| 170 | 
            -
             | 
| 171 | 
            -
             | 
| 172 | 
            -
                         | 
| 173 | 
            -
                         | 
| 174 | 
            -
                         | 
| 175 | 
            -
                         | 
| 176 | 
            -
                         | 
| 177 | 
            -
             | 
| 178 | 
            -
                 | 
| 179 | 
            -
                 | 
| 180 | 
            -
             | 
| 181 | 
            -
                     | 
| 182 | 
            -
             | 
| 183 | 
            -
             | 
| 184 | 
            -
             | 
| 185 | 
            -
             | 
| 186 | 
            -
                    " | 
| 187 | 
            -
                    " | 
| 188 | 
            -
                    " | 
| 189 | 
            -
                    " | 
| 190 | 
            -
             | 
| 191 | 
            -
                
         | 
| 192 | 
            -
                 | 
| 193 | 
            -
             | 
| 194 | 
            -
                     | 
| 195 | 
            -
                     | 
| 196 | 
            -
                     | 
| 197 | 
            -
                     | 
| 198 | 
            -
                     | 
| 199 | 
            -
             | 
| 200 | 
            -
                
         | 
| 201 | 
            -
                 | 
| 202 | 
            -
                 | 
| 203 | 
            -
                
         | 
| 204 | 
            -
                 | 
| 205 | 
            -
             | 
| 206 | 
            -
                     | 
| 207 | 
            -
             | 
| 208 | 
            -
             | 
| 209 | 
            -
             | 
| 210 | 
            -
                         | 
| 211 | 
            -
             | 
| 212 | 
            -
             | 
| 213 | 
            -
             | 
| 214 | 
            -
             | 
| 215 | 
            -
                     | 
| 216 | 
            -
             | 
| 217 | 
            -
             | 
| 218 | 
            -
             | 
| 219 | 
            -
                 | 
| 220 | 
            -
             | 
| 221 | 
            -
                     | 
| 222 | 
            -
                     | 
| 223 | 
            -
             | 
| 224 | 
            -
                 | 
| 225 | 
            -
             | 
| 226 | 
            -
                     | 
| 227 | 
            -
                     | 
| 228 | 
            -
             | 
| 229 | 
            -
                
         | 
| 230 | 
            -
             | 
| 231 | 
            -
             | 
|  | 
|  | |
| 1 | 
            +
            from dataclasses import dataclass, field
         | 
| 2 | 
            +
            from typing import List, Any
         | 
| 3 | 
            +
            import gradio as gr
         | 
| 4 | 
            +
            from gradio_imagemeta import ImageMeta
         | 
| 5 | 
            +
            from gradio_imagemeta.helpers import extract_metadata, add_metadata, transfer_metadata
         | 
| 6 | 
            +
            from gradio_propertysheet import PropertySheet
         | 
| 7 | 
            +
            from gradio_propertysheet.helpers import build_dataclass_fields, create_dataclass_instance
         | 
| 8 | 
            +
            from pathlib import Path
         | 
| 9 | 
            +
             | 
| 10 | 
            +
             | 
| 11 | 
            +
            output_dir = Path("outputs")
         | 
| 12 | 
            +
            output_dir.mkdir(exist_ok=True)
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            @dataclass
         | 
| 15 | 
            +
            class ImageSettings:
         | 
| 16 | 
            +
                """Configuration for image metadata settings."""
         | 
| 17 | 
            +
                model: str = field(default="", metadata={"label": "Model"})
         | 
| 18 | 
            +
                f_number: str = field(default="", metadata={"label": "FNumber"})
         | 
| 19 | 
            +
                iso_speed_ratings: str = field(default="", metadata={"label": "ISOSpeedRatings"})
         | 
| 20 | 
            +
                s_churn: float = field(
         | 
| 21 | 
            +
                    default=0.0,
         | 
| 22 | 
            +
                    metadata={"component": "slider", "label": "Schurn", "minimum": 0.0, "maximum": 1.0, "step": 0.01},
         | 
| 23 | 
            +
                )
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            @dataclass
         | 
| 26 | 
            +
            class PropertyConfig:
         | 
| 27 | 
            +
                """Root configuration for image properties, including nested image settings."""
         | 
| 28 | 
            +
                image_settings: ImageSettings = field(default_factory=ImageSettings)
         | 
| 29 | 
            +
                description: str = field(default="", metadata={"label": "Description"})
         | 
| 30 | 
            +
             | 
| 31 | 
            +
            def infer_type(s: str):
         | 
| 32 | 
            +
                """
         | 
| 33 | 
            +
                Infers and converts a string to the most likely data type.
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                It attempts conversions in the following order:
         | 
| 36 | 
            +
                1. Integer
         | 
| 37 | 
            +
                2. Float
         | 
| 38 | 
            +
                3. Boolean (case-insensitive 'true' or 'false')
         | 
| 39 | 
            +
                If all conversions fail, it returns the original string.
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                Args:
         | 
| 42 | 
            +
                    s: The input string to be converted.
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                Returns:
         | 
| 45 | 
            +
                    The converted value (int, float, bool) or the original string.
         | 
| 46 | 
            +
                """
         | 
| 47 | 
            +
                if not isinstance(s, str):
         | 
| 48 | 
            +
                    # If the input is not a string, return it as is.
         | 
| 49 | 
            +
                    return s
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                # 1. Try to convert to an integer
         | 
| 52 | 
            +
                try:
         | 
| 53 | 
            +
                    return int(s)
         | 
| 54 | 
            +
                except ValueError:
         | 
| 55 | 
            +
                    # Not an integer, continue...
         | 
| 56 | 
            +
                    pass
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                # 2. Try to convert to a float
         | 
| 59 | 
            +
                try:
         | 
| 60 | 
            +
                    return float(s)
         | 
| 61 | 
            +
                except ValueError:
         | 
| 62 | 
            +
                    # Not a float, continue...
         | 
| 63 | 
            +
                    pass
         | 
| 64 | 
            +
                
         | 
| 65 | 
            +
                # 3. Check for a boolean value
         | 
| 66 | 
            +
                # This explicit check is important because bool('False') evaluates to True.
         | 
| 67 | 
            +
                s_lower = s.lower()
         | 
| 68 | 
            +
                if s_lower == 'true':
         | 
| 69 | 
            +
                    return True
         | 
| 70 | 
            +
                if s_lower == 'false':
         | 
| 71 | 
            +
                    return False
         | 
| 72 | 
            +
                    
         | 
| 73 | 
            +
                # 4. If nothing else worked, return the original string
         | 
| 74 | 
            +
                return s
         | 
| 75 | 
            +
             | 
| 76 | 
            +
            def handle_load_metadata(image_data: ImageMeta | None) -> List[Any]:
         | 
| 77 | 
            +
                """
         | 
| 78 | 
            +
                Processes image metadata and maps it to output components.
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                Args:
         | 
| 81 | 
            +
                    image_data: ImageMeta object containing image data and metadata, or None.
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                Returns:
         | 
| 84 | 
            +
                    A list of values for output components (Textbox, Slider, or PropertySheet instances).
         | 
| 85 | 
            +
                """
         | 
| 86 | 
            +
                if not image_data:
         | 
| 87 | 
            +
                    return [gr.Textbox(value="") for _ in output_fields]
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                metadata = extract_metadata(image_data, only_custom_metadata=True)
         | 
| 90 | 
            +
                dataclass_fields = build_dataclass_fields(PropertyConfig)
         | 
| 91 | 
            +
                raw_values = transfer_metadata(output_fields, metadata, dataclass_fields)
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                output_values = [gr.skip()] * len(output_fields)
         | 
| 94 | 
            +
                for i, (component, value) in enumerate(zip(output_fields, raw_values)):        
         | 
| 95 | 
            +
                    if hasattr(component, 'root_label'):
         | 
| 96 | 
            +
                        output_values[i] = create_dataclass_instance(PropertyConfig, value)
         | 
| 97 | 
            +
                    else:
         | 
| 98 | 
            +
                        output_values[i] = gr.update(value=infer_type(value))
         | 
| 99 | 
            +
                
         | 
| 100 | 
            +
                return output_values
         | 
| 101 | 
            +
             | 
| 102 | 
            +
            def save_image_with_metadata(image_data: Any, *inputs: Any) -> str | None:
         | 
| 103 | 
            +
                """
         | 
| 104 | 
            +
                Saves an image with updated metadata to a file.
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                Args:
         | 
| 107 | 
            +
                    image_data: Input image data (e.g., file path or PIL Image).
         | 
| 108 | 
            +
                    *inputs: Variable number of input values from UI components (Textbox, Slider).
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                Returns:
         | 
| 111 | 
            +
                    The file path of the saved image, or None if no image is provided.
         | 
| 112 | 
            +
                """
         | 
| 113 | 
            +
                if not image_data:
         | 
| 114 | 
            +
                    return None
         | 
| 115 | 
            +
                
         | 
| 116 | 
            +
                params = list(inputs)
         | 
| 117 | 
            +
                image_params = dict(zip(input_fields.keys(), params))    
         | 
| 118 | 
            +
                metadata = {label: image_params.get(label, "") for label in image_params.keys()}
         | 
| 119 | 
            +
                
         | 
| 120 | 
            +
                new_filepath = output_dir / "image_with_meta.png"
         | 
| 121 | 
            +
                
         | 
| 122 | 
            +
                add_metadata(image_data, metadata, new_filepath)
         | 
| 123 | 
            +
                
         | 
| 124 | 
            +
                return str(new_filepath)
         | 
| 125 | 
            +
             | 
| 126 | 
            +
            initial_property_from_meta_config = PropertyConfig()
         | 
| 127 | 
            +
             | 
| 128 | 
            +
            with gr.Blocks() as demo:
         | 
| 129 | 
            +
                gr.Markdown("# ImageMeta Component Demo")
         | 
| 130 | 
            +
                gr.Markdown(
         | 
| 131 | 
            +
                    """
         | 
| 132 | 
            +
                    **To Test:**
         | 
| 133 | 
            +
                    1. Upload an image with EXIF or PNG metadata using either the "Upload Imagem (Custom metadata only)" component or the "Upload Imagem (all metadata)" component.
         | 
| 134 | 
            +
                    2. Click the 'Info' icon (ⓘ) in the top-left of the image component to view the metadata panel.
         | 
| 135 | 
            +
                    3. Click 'Load Metadata' in the popup to populate the fields below with metadata values (`Model`, `FNumber`, `ISOSpeedRatings`, `Schurn`, `Description`).
         | 
| 136 | 
            +
                    4. The section below displays how metadata is rendered in components and the `PropertySheet` custom component, showing the hierarchical structure of the image settings.
         | 
| 137 | 
            +
                    5. In the "Metadata Viewer" section, you can add field values as metadata to a previously uploaded image in "Upload Image (Custom metadata only)." Then click 'Add metadata and save image' to save a new image with the metadata.
         | 
| 138 | 
            +
                    """
         | 
| 139 | 
            +
                )
         | 
| 140 | 
            +
                property_sheet_state = gr.State(value=initial_property_from_meta_config)
         | 
| 141 | 
            +
                with gr.Row():
         | 
| 142 | 
            +
                    img_custom = ImageMeta(
         | 
| 143 | 
            +
                        label="Upload Image (Custom metadata only)",
         | 
| 144 | 
            +
                        type="filepath",
         | 
| 145 | 
            +
                        width=300,
         | 
| 146 | 
            +
                        height=400,            
         | 
| 147 | 
            +
                        interactive=True
         | 
| 148 | 
            +
                    )
         | 
| 149 | 
            +
                    img_all = ImageMeta(
         | 
| 150 | 
            +
                        label="Upload Image (All metadata)",
         | 
| 151 | 
            +
                        only_custom_metadata=False,
         | 
| 152 | 
            +
                        type="filepath",
         | 
| 153 | 
            +
                        width=300,
         | 
| 154 | 
            +
                        height=400,            
         | 
| 155 | 
            +
                        popup_metadata_height=400,
         | 
| 156 | 
            +
                        popup_metadata_width=500,
         | 
| 157 | 
            +
                        interactive=True
         | 
| 158 | 
            +
                    )
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                gr.Markdown("## Metadata Viewer")
         | 
| 161 | 
            +
                gr.Markdown("### Individual Components")
         | 
| 162 | 
            +
                with gr.Row():
         | 
| 163 | 
            +
                    model_box = gr.Textbox(label="Model")
         | 
| 164 | 
            +
                    fnumber_box = gr.Textbox(label="FNumber")
         | 
| 165 | 
            +
                    iso_box = gr.Textbox(label="ISOSpeedRatings")
         | 
| 166 | 
            +
                    s_churn = gr.Slider(label="Schurn", value=1.0, minimum=0.0, maximum=1.0, step=0.1)
         | 
| 167 | 
            +
                    description_box = gr.Textbox(label="Description")
         | 
| 168 | 
            +
                
         | 
| 169 | 
            +
                gr.Markdown("### PropertySheet Component")
         | 
| 170 | 
            +
                with gr.Row():
         | 
| 171 | 
            +
                    property_sheet = PropertySheet(
         | 
| 172 | 
            +
                        value=initial_property_from_meta_config,
         | 
| 173 | 
            +
                        label="Image Settings",
         | 
| 174 | 
            +
                        width=400,
         | 
| 175 | 
            +
                        height=550,
         | 
| 176 | 
            +
                        visible=True,
         | 
| 177 | 
            +
                        root_label="General"
         | 
| 178 | 
            +
                    )    
         | 
| 179 | 
            +
                gr.Markdown("## Metadata Editor")
         | 
| 180 | 
            +
                with gr.Row():
         | 
| 181 | 
            +
                    save_button = gr.Button("Add Metadata and Save Image")
         | 
| 182 | 
            +
                    saved_file_output = gr.File(label="Download Image")
         | 
| 183 | 
            +
               
         | 
| 184 | 
            +
                    
         | 
| 185 | 
            +
                input_fields = {
         | 
| 186 | 
            +
                    "Model": model_box,
         | 
| 187 | 
            +
                    "FNumber": fnumber_box,
         | 
| 188 | 
            +
                    "ISOSpeedRatings": iso_box,
         | 
| 189 | 
            +
                    "Schurn": s_churn,
         | 
| 190 | 
            +
                    "Description": description_box
         | 
| 191 | 
            +
                }
         | 
| 192 | 
            +
                
         | 
| 193 | 
            +
                output_fields = [
         | 
| 194 | 
            +
                    property_sheet,
         | 
| 195 | 
            +
                    model_box,
         | 
| 196 | 
            +
                    fnumber_box,
         | 
| 197 | 
            +
                    iso_box,
         | 
| 198 | 
            +
                    s_churn,
         | 
| 199 | 
            +
                    description_box
         | 
| 200 | 
            +
                ]
         | 
| 201 | 
            +
                
         | 
| 202 | 
            +
                img_custom.load_metadata(handle_load_metadata, inputs=img_custom, outputs=output_fields)
         | 
| 203 | 
            +
                img_all.load_metadata(handle_load_metadata, inputs=img_all, outputs=output_fields)
         | 
| 204 | 
            +
                
         | 
| 205 | 
            +
                def handle_render_change(updated_config: PropertyConfig, current_state: PropertyConfig):
         | 
| 206 | 
            +
                    """
         | 
| 207 | 
            +
                    Updates the PropertySheet state when its configuration changes.
         | 
| 208 | 
            +
             | 
| 209 | 
            +
                    Args:
         | 
| 210 | 
            +
                        updated_config: The new PropertyConfig instance from the PropertySheet.
         | 
| 211 | 
            +
                        current_state: The current PropertyConfig state.
         | 
| 212 | 
            +
             | 
| 213 | 
            +
                    Returns:
         | 
| 214 | 
            +
                        A tuple of (updated_config, updated_config) or (current_state, current_state) if updated_config is None.
         | 
| 215 | 
            +
                    """
         | 
| 216 | 
            +
                    if updated_config is None:
         | 
| 217 | 
            +
                        return current_state, current_state
         | 
| 218 | 
            +
                    return updated_config, updated_config
         | 
| 219 | 
            +
                
         | 
| 220 | 
            +
                property_sheet.change(
         | 
| 221 | 
            +
                    fn=handle_render_change,
         | 
| 222 | 
            +
                    inputs=[property_sheet, property_sheet_state],
         | 
| 223 | 
            +
                    outputs=[property_sheet, property_sheet_state]
         | 
| 224 | 
            +
                )
         | 
| 225 | 
            +
                save_button.click(
         | 
| 226 | 
            +
                    save_image_with_metadata,
         | 
| 227 | 
            +
                    inputs=[img_custom, *input_fields.values()],
         | 
| 228 | 
            +
                    outputs=[saved_file_output]
         | 
| 229 | 
            +
                )
         | 
| 230 | 
            +
                
         | 
| 231 | 
            +
            if __name__ == "__main__":
         | 
| 232 | 
            +
                demo.launch()
         | 
    	
        space.py
    CHANGED
    
    | @@ -21,7 +21,7 @@ with gr.Blocks( | |
| 21 | 
             
            # `gradio_imagemeta`
         | 
| 22 |  | 
| 23 | 
             
            <div style="display: flex; gap: 7px;">
         | 
| 24 | 
            -
            <img alt=" | 
| 25 | 
             
            </div>
         | 
| 26 |  | 
| 27 | 
             
            Image Preview with Metadata for Gradio Interface
         | 
| @@ -47,6 +47,7 @@ from gradio_propertysheet import PropertySheet | |
| 47 | 
             
            from gradio_propertysheet.helpers import build_dataclass_fields, create_dataclass_instance
         | 
| 48 | 
             
            from pathlib import Path
         | 
| 49 |  | 
|  | |
| 50 | 
             
            output_dir = Path("outputs")
         | 
| 51 | 
             
            output_dir.mkdir(exist_ok=True)
         | 
| 52 |  | 
| @@ -67,25 +68,50 @@ class PropertyConfig: | |
| 67 | 
             
                image_settings: ImageSettings = field(default_factory=ImageSettings)
         | 
| 68 | 
             
                description: str = field(default="", metadata={"label": "Description"})
         | 
| 69 |  | 
| 70 | 
            -
            def  | 
| 71 | 
             
                \"\"\"
         | 
| 72 | 
            -
                 | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 73 |  | 
| 74 | 
             
                Args:
         | 
| 75 | 
            -
                     | 
| 76 | 
            -
                    img_all_path: File path for the image to display in img_all.
         | 
| 77 |  | 
| 78 | 
             
                Returns:
         | 
| 79 | 
            -
                     | 
| 80 | 
             
                \"\"\"
         | 
| 81 | 
            -
                 | 
| 82 | 
            -
             | 
| 83 | 
            -
                     | 
| 84 | 
            -
             | 
| 85 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 86 |  | 
| 87 | 
            -
                #  | 
| 88 | 
            -
                 | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 89 |  | 
| 90 | 
             
            def handle_load_metadata(image_data: ImageMeta | None) -> List[Any]:
         | 
| 91 | 
             
                \"\"\"
         | 
| @@ -105,11 +131,11 @@ def handle_load_metadata(image_data: ImageMeta | None) -> List[Any]: | |
| 105 | 
             
                raw_values = transfer_metadata(output_fields, metadata, dataclass_fields)
         | 
| 106 |  | 
| 107 | 
             
                output_values = [gr.skip()] * len(output_fields)
         | 
| 108 | 
            -
                for i, (component, value) in enumerate(zip(output_fields, raw_values)):
         | 
| 109 | 
             
                    if hasattr(component, 'root_label'):
         | 
| 110 | 
             
                        output_values[i] = create_dataclass_instance(PropertyConfig, value)
         | 
| 111 | 
             
                    else:
         | 
| 112 | 
            -
                        output_values[i] = gr. | 
| 113 |  | 
| 114 | 
             
                return output_values
         | 
| 115 |  | 
| @@ -129,10 +155,11 @@ def save_image_with_metadata(image_data: Any, *inputs: Any) -> str | None: | |
| 129 |  | 
| 130 | 
             
                params = list(inputs)
         | 
| 131 | 
             
                image_params = dict(zip(input_fields.keys(), params))
         | 
| 132 | 
            -
                dataclass_fields = build_dataclass_fields(PropertyConfig)
         | 
| 133 | 
            -
                metadata = {label: image_params.get(label, "") for label in  | 
| 134 |  | 
| 135 | 
             
                new_filepath = output_dir / "image_with_meta.png"
         | 
|  | |
| 136 | 
             
                add_metadata(image_data, metadata, new_filepath)
         | 
| 137 |  | 
| 138 | 
             
                return str(new_filepath)
         | 
| @@ -157,17 +184,18 @@ with gr.Blocks() as demo: | |
| 157 | 
             
                        label="Upload Image (Custom metadata only)",
         | 
| 158 | 
             
                        type="filepath",
         | 
| 159 | 
             
                        width=300,
         | 
| 160 | 
            -
                        height=400,
         | 
| 161 | 
            -
                        disable_preprocess=False,
         | 
| 162 | 
             
                        interactive=True
         | 
| 163 | 
             
                    )
         | 
| 164 | 
             
                    img_all = ImageMeta(
         | 
| 165 | 
             
                        label="Upload Image (All metadata)",
         | 
| 166 | 
             
                        only_custom_metadata=False,
         | 
|  | |
| 167 | 
             
                        width=300,
         | 
| 168 | 
            -
                        height=400,
         | 
| 169 | 
             
                        popup_metadata_height=400,
         | 
| 170 | 
            -
                        popup_metadata_width=500
         | 
|  | |
| 171 | 
             
                    )
         | 
| 172 |  | 
| 173 | 
             
                gr.Markdown("## Metadata Viewer")
         | 
| @@ -193,17 +221,7 @@ with gr.Blocks() as demo: | |
| 193 | 
             
                with gr.Row():
         | 
| 194 | 
             
                    save_button = gr.Button("Add Metadata and Save Image")
         | 
| 195 | 
             
                    saved_file_output = gr.File(label="Download Image")
         | 
| 196 | 
            -
             | 
| 197 | 
            -
                with gr.Row():
         | 
| 198 | 
            -
                    gr.Examples(
         | 
| 199 | 
            -
                        examples=[
         | 
| 200 | 
            -
                            ["./examples/image_with_meta.png", "./examples/image_with_meta.png"]
         | 
| 201 | 
            -
                        ],
         | 
| 202 | 
            -
                        fn=process_example_images,
         | 
| 203 | 
            -
                        inputs=[img_custom, img_all],
         | 
| 204 | 
            -
                        outputs=[img_custom, img_all],
         | 
| 205 | 
            -
                        cache_examples=True
         | 
| 206 | 
            -
                    )
         | 
| 207 |  | 
| 208 | 
             
                input_fields = {
         | 
| 209 | 
             
                    "Model": model_box,
         | 
|  | |
| 21 | 
             
            # `gradio_imagemeta`
         | 
| 22 |  | 
| 23 | 
             
            <div style="display: flex; gap: 7px;">
         | 
| 24 | 
            +
            <a href="https://pypi.org/project/gradio_imagemeta/" target="_blank"><img alt="PyPI - Version" src="https://img.shields.io/pypi/v/gradio_imagemeta"></a>  
         | 
| 25 | 
             
            </div>
         | 
| 26 |  | 
| 27 | 
             
            Image Preview with Metadata for Gradio Interface
         | 
|  | |
| 47 | 
             
            from gradio_propertysheet.helpers import build_dataclass_fields, create_dataclass_instance
         | 
| 48 | 
             
            from pathlib import Path
         | 
| 49 |  | 
| 50 | 
            +
             | 
| 51 | 
             
            output_dir = Path("outputs")
         | 
| 52 | 
             
            output_dir.mkdir(exist_ok=True)
         | 
| 53 |  | 
|  | |
| 68 | 
             
                image_settings: ImageSettings = field(default_factory=ImageSettings)
         | 
| 69 | 
             
                description: str = field(default="", metadata={"label": "Description"})
         | 
| 70 |  | 
| 71 | 
            +
            def infer_type(s: str):
         | 
| 72 | 
             
                \"\"\"
         | 
| 73 | 
            +
                Infers and converts a string to the most likely data type.
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                It attempts conversions in the following order:
         | 
| 76 | 
            +
                1. Integer
         | 
| 77 | 
            +
                2. Float
         | 
| 78 | 
            +
                3. Boolean (case-insensitive 'true' or 'false')
         | 
| 79 | 
            +
                If all conversions fail, it returns the original string.
         | 
| 80 |  | 
| 81 | 
             
                Args:
         | 
| 82 | 
            +
                    s: The input string to be converted.
         | 
|  | |
| 83 |  | 
| 84 | 
             
                Returns:
         | 
| 85 | 
            +
                    The converted value (int, float, bool) or the original string.
         | 
| 86 | 
             
                \"\"\"
         | 
| 87 | 
            +
                if not isinstance(s, str):
         | 
| 88 | 
            +
                    # If the input is not a string, return it as is.
         | 
| 89 | 
            +
                    return s
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                # 1. Try to convert to an integer
         | 
| 92 | 
            +
                try:
         | 
| 93 | 
            +
                    return int(s)
         | 
| 94 | 
            +
                except ValueError:
         | 
| 95 | 
            +
                    # Not an integer, continue...
         | 
| 96 | 
            +
                    pass
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                # 2. Try to convert to a float
         | 
| 99 | 
            +
                try:
         | 
| 100 | 
            +
                    return float(s)
         | 
| 101 | 
            +
                except ValueError:
         | 
| 102 | 
            +
                    # Not a float, continue...
         | 
| 103 | 
            +
                    pass
         | 
| 104 |  | 
| 105 | 
            +
                # 3. Check for a boolean value
         | 
| 106 | 
            +
                # This explicit check is important because bool('False') evaluates to True.
         | 
| 107 | 
            +
                s_lower = s.lower()
         | 
| 108 | 
            +
                if s_lower == 'true':
         | 
| 109 | 
            +
                    return True
         | 
| 110 | 
            +
                if s_lower == 'false':
         | 
| 111 | 
            +
                    return False
         | 
| 112 | 
            +
                    
         | 
| 113 | 
            +
                # 4. If nothing else worked, return the original string
         | 
| 114 | 
            +
                return s
         | 
| 115 |  | 
| 116 | 
             
            def handle_load_metadata(image_data: ImageMeta | None) -> List[Any]:
         | 
| 117 | 
             
                \"\"\"
         | 
|  | |
| 131 | 
             
                raw_values = transfer_metadata(output_fields, metadata, dataclass_fields)
         | 
| 132 |  | 
| 133 | 
             
                output_values = [gr.skip()] * len(output_fields)
         | 
| 134 | 
            +
                for i, (component, value) in enumerate(zip(output_fields, raw_values)):        
         | 
| 135 | 
             
                    if hasattr(component, 'root_label'):
         | 
| 136 | 
             
                        output_values[i] = create_dataclass_instance(PropertyConfig, value)
         | 
| 137 | 
             
                    else:
         | 
| 138 | 
            +
                        output_values[i] = gr.update(value=infer_type(value))
         | 
| 139 |  | 
| 140 | 
             
                return output_values
         | 
| 141 |  | 
|  | |
| 155 |  | 
| 156 | 
             
                params = list(inputs)
         | 
| 157 | 
             
                image_params = dict(zip(input_fields.keys(), params))
         | 
| 158 | 
            +
                #dataclass_fields = build_dataclass_fields(PropertyConfig)
         | 
| 159 | 
            +
                metadata = {label: image_params.get(label, "") for label in image_params.keys()}
         | 
| 160 |  | 
| 161 | 
             
                new_filepath = output_dir / "image_with_meta.png"
         | 
| 162 | 
            +
                
         | 
| 163 | 
             
                add_metadata(image_data, metadata, new_filepath)
         | 
| 164 |  | 
| 165 | 
             
                return str(new_filepath)
         | 
|  | |
| 184 | 
             
                        label="Upload Image (Custom metadata only)",
         | 
| 185 | 
             
                        type="filepath",
         | 
| 186 | 
             
                        width=300,
         | 
| 187 | 
            +
                        height=400,            
         | 
|  | |
| 188 | 
             
                        interactive=True
         | 
| 189 | 
             
                    )
         | 
| 190 | 
             
                    img_all = ImageMeta(
         | 
| 191 | 
             
                        label="Upload Image (All metadata)",
         | 
| 192 | 
             
                        only_custom_metadata=False,
         | 
| 193 | 
            +
                        type="filepath",
         | 
| 194 | 
             
                        width=300,
         | 
| 195 | 
            +
                        height=400,            
         | 
| 196 | 
             
                        popup_metadata_height=400,
         | 
| 197 | 
            +
                        popup_metadata_width=500,
         | 
| 198 | 
            +
                        interactive=True
         | 
| 199 | 
             
                    )
         | 
| 200 |  | 
| 201 | 
             
                gr.Markdown("## Metadata Viewer")
         | 
|  | |
| 221 | 
             
                with gr.Row():
         | 
| 222 | 
             
                    save_button = gr.Button("Add Metadata and Save Image")
         | 
| 223 | 
             
                    saved_file_output = gr.File(label="Download Image")
         | 
| 224 | 
            +
               
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 225 |  | 
| 226 | 
             
                input_fields = {
         | 
| 227 | 
             
                    "Model": model_box,
         | 
    	
        src/.gitignore
    CHANGED
    
    | @@ -12,4 +12,5 @@ __tmp/* | |
| 12 | 
             
            .ruff_cache
         | 
| 13 | 
             
            node_modules
         | 
| 14 | 
             
            backend/**/templates/
         | 
| 15 | 
            -
            outputs/
         | 
|  | 
|  | |
| 12 | 
             
            .ruff_cache
         | 
| 13 | 
             
            node_modules
         | 
| 14 | 
             
            backend/**/templates/
         | 
| 15 | 
            +
            outputs/
         | 
| 16 | 
            +
            README_TEMPLATE.md
         | 
    	
        src/README.md
    CHANGED
    
    | @@ -10,12 +10,23 @@ app_file: space.py | |
| 10 | 
             
            ---
         | 
| 11 |  | 
| 12 | 
             
            # `gradio_imagemeta`
         | 
| 13 | 
            -
            <img alt="Static Badge" src="https://img.shields.io/badge/version%20-%200.0.1%20-% | 
| 14 |  | 
| 15 | 
             
            Image Preview with Metadata for Gradio Interface
         | 
| 16 |  | 
| 17 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 18 |  | 
|  | |
| 19 | 
             
            ```bash
         | 
| 20 | 
             
            pip install gradio_imagemeta
         | 
| 21 | 
             
            ```
         | 
| @@ -32,6 +43,7 @@ from gradio_propertysheet import PropertySheet | |
| 32 | 
             
            from gradio_propertysheet.helpers import build_dataclass_fields, create_dataclass_instance
         | 
| 33 | 
             
            from pathlib import Path
         | 
| 34 |  | 
|  | |
| 35 | 
             
            output_dir = Path("outputs")
         | 
| 36 | 
             
            output_dir.mkdir(exist_ok=True)
         | 
| 37 |  | 
| @@ -52,25 +64,50 @@ class PropertyConfig: | |
| 52 | 
             
                image_settings: ImageSettings = field(default_factory=ImageSettings)
         | 
| 53 | 
             
                description: str = field(default="", metadata={"label": "Description"})
         | 
| 54 |  | 
| 55 | 
            -
            def  | 
| 56 | 
             
                """
         | 
| 57 | 
            -
                 | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 58 |  | 
| 59 | 
             
                Args:
         | 
| 60 | 
            -
                     | 
| 61 | 
            -
                    img_all_path: File path for the image to display in img_all.
         | 
| 62 |  | 
| 63 | 
             
                Returns:
         | 
| 64 | 
            -
                     | 
| 65 | 
             
                """
         | 
| 66 | 
            -
                 | 
| 67 | 
            -
             | 
| 68 | 
            -
                     | 
| 69 | 
            -
             | 
| 70 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 71 |  | 
| 72 | 
            -
                #  | 
| 73 | 
            -
                 | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 74 |  | 
| 75 | 
             
            def handle_load_metadata(image_data: ImageMeta | None) -> List[Any]:
         | 
| 76 | 
             
                """
         | 
| @@ -90,11 +127,11 @@ def handle_load_metadata(image_data: ImageMeta | None) -> List[Any]: | |
| 90 | 
             
                raw_values = transfer_metadata(output_fields, metadata, dataclass_fields)
         | 
| 91 |  | 
| 92 | 
             
                output_values = [gr.skip()] * len(output_fields)
         | 
| 93 | 
            -
                for i, (component, value) in enumerate(zip(output_fields, raw_values)):
         | 
| 94 | 
             
                    if hasattr(component, 'root_label'):
         | 
| 95 | 
             
                        output_values[i] = create_dataclass_instance(PropertyConfig, value)
         | 
| 96 | 
             
                    else:
         | 
| 97 | 
            -
                        output_values[i] = gr. | 
| 98 |  | 
| 99 | 
             
                return output_values
         | 
| 100 |  | 
| @@ -114,10 +151,11 @@ def save_image_with_metadata(image_data: Any, *inputs: Any) -> str | None: | |
| 114 |  | 
| 115 | 
             
                params = list(inputs)
         | 
| 116 | 
             
                image_params = dict(zip(input_fields.keys(), params))
         | 
| 117 | 
            -
                dataclass_fields = build_dataclass_fields(PropertyConfig)
         | 
| 118 | 
            -
                metadata = {label: image_params.get(label, "") for label in  | 
| 119 |  | 
| 120 | 
             
                new_filepath = output_dir / "image_with_meta.png"
         | 
|  | |
| 121 | 
             
                add_metadata(image_data, metadata, new_filepath)
         | 
| 122 |  | 
| 123 | 
             
                return str(new_filepath)
         | 
| @@ -142,17 +180,18 @@ with gr.Blocks() as demo: | |
| 142 | 
             
                        label="Upload Image (Custom metadata only)",
         | 
| 143 | 
             
                        type="filepath",
         | 
| 144 | 
             
                        width=300,
         | 
| 145 | 
            -
                        height=400,
         | 
| 146 | 
            -
                        disable_preprocess=False,
         | 
| 147 | 
             
                        interactive=True
         | 
| 148 | 
             
                    )
         | 
| 149 | 
             
                    img_all = ImageMeta(
         | 
| 150 | 
             
                        label="Upload Image (All metadata)",
         | 
| 151 | 
             
                        only_custom_metadata=False,
         | 
|  | |
| 152 | 
             
                        width=300,
         | 
| 153 | 
            -
                        height=400,
         | 
| 154 | 
             
                        popup_metadata_height=400,
         | 
| 155 | 
            -
                        popup_metadata_width=500
         | 
|  | |
| 156 | 
             
                    )
         | 
| 157 |  | 
| 158 | 
             
                gr.Markdown("## Metadata Viewer")
         | 
| @@ -178,17 +217,7 @@ with gr.Blocks() as demo: | |
| 178 | 
             
                with gr.Row():
         | 
| 179 | 
             
                    save_button = gr.Button("Add Metadata and Save Image")
         | 
| 180 | 
             
                    saved_file_output = gr.File(label="Download Image")
         | 
| 181 | 
            -
             | 
| 182 | 
            -
                with gr.Row():
         | 
| 183 | 
            -
                    gr.Examples(
         | 
| 184 | 
            -
                        examples=[
         | 
| 185 | 
            -
                            ["./examples/image_with_meta.png", "./examples/image_with_meta.png"]
         | 
| 186 | 
            -
                        ],
         | 
| 187 | 
            -
                        fn=process_example_images,
         | 
| 188 | 
            -
                        inputs=[img_custom, img_all],
         | 
| 189 | 
            -
                        outputs=[img_custom, img_all],
         | 
| 190 | 
            -
                        cache_examples=True
         | 
| 191 | 
            -
                    )
         | 
| 192 |  | 
| 193 | 
             
                input_fields = {
         | 
| 194 | 
             
                    "Model": model_box,
         | 
|  | |
| 10 | 
             
            ---
         | 
| 11 |  | 
| 12 | 
             
            # `gradio_imagemeta`
         | 
| 13 | 
            +
            <img alt="Static Badge" src="https://img.shields.io/badge/version%20-%200.0.1%20-%20blue"> <a href="https://huggingface.co/spaces/elismasilva/gradio_imagemeta"><img src="https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Demo-blue"></a><p><span>💻 <a href='https://github.com/DEVAIEXP/gradio_component_imagemeta'>Component GitHub Code</a></span></p>
         | 
| 14 |  | 
| 15 | 
             
            Image Preview with Metadata for Gradio Interface
         | 
| 16 |  | 
| 17 | 
            +
            Imagine loading a photo with embedded presets (e.g., camera settings or AI model parameters) and instantly populating your app’s UI with those values. With ImageMeta, you can extract and apply these presets effortlessly, streamlining workflows for photographers, data scientists, or creative professionals.
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            # Features and Key Characteristics
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            **ImageMeta** is a custom Gradio component designed to enhance image handling with robust metadata support. Key features include:
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            - **Interactive Image Upload**: Upload PNG/JPEG images via drag-and-drop or file selection, with real-time metadata extraction using `exifr` on the client side.
         | 
| 24 | 
            +
            - **Metadata Extraction & Display**: View EXIF, IPTC, and XMP metadata in a customizable popup, with options to filter custom metadata (e.g., `Model`, `Schurn`) or include technical details (e.g., `ImageWidth`).
         | 
| 25 | 
            +
            - **Preset Loading**: Load metadata directly into UI components (Textbox, Slider, PropertySheet) to apply saved presets, streamlining workflows like camera settings or AI model parameters.
         | 
| 26 | 
            +
            - **Metadata Editing & Saving**: Add or update metadata and save images with embedded metadata for downstream use, powered by PIL on the server side.
         | 
| 27 | 
            +
            - **Responsive Design**: Supports fullscreen mode, adjustable popup sizes, and a polished Svelte-based UI for seamless user experiences.
         | 
| 28 |  | 
| 29 | 
            +
            ## Installation
         | 
| 30 | 
             
            ```bash
         | 
| 31 | 
             
            pip install gradio_imagemeta
         | 
| 32 | 
             
            ```
         | 
|  | |
| 43 | 
             
            from gradio_propertysheet.helpers import build_dataclass_fields, create_dataclass_instance
         | 
| 44 | 
             
            from pathlib import Path
         | 
| 45 |  | 
| 46 | 
            +
             | 
| 47 | 
             
            output_dir = Path("outputs")
         | 
| 48 | 
             
            output_dir.mkdir(exist_ok=True)
         | 
| 49 |  | 
|  | |
| 64 | 
             
                image_settings: ImageSettings = field(default_factory=ImageSettings)
         | 
| 65 | 
             
                description: str = field(default="", metadata={"label": "Description"})
         | 
| 66 |  | 
| 67 | 
            +
            def infer_type(s: str):
         | 
| 68 | 
             
                """
         | 
| 69 | 
            +
                Infers and converts a string to the most likely data type.
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                It attempts conversions in the following order:
         | 
| 72 | 
            +
                1. Integer
         | 
| 73 | 
            +
                2. Float
         | 
| 74 | 
            +
                3. Boolean (case-insensitive 'true' or 'false')
         | 
| 75 | 
            +
                If all conversions fail, it returns the original string.
         | 
| 76 |  | 
| 77 | 
             
                Args:
         | 
| 78 | 
            +
                    s: The input string to be converted.
         | 
|  | |
| 79 |  | 
| 80 | 
             
                Returns:
         | 
| 81 | 
            +
                    The converted value (int, float, bool) or the original string.
         | 
| 82 | 
             
                """
         | 
| 83 | 
            +
                if not isinstance(s, str):
         | 
| 84 | 
            +
                    # If the input is not a string, return it as is.
         | 
| 85 | 
            +
                    return s
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                # 1. Try to convert to an integer
         | 
| 88 | 
            +
                try:
         | 
| 89 | 
            +
                    return int(s)
         | 
| 90 | 
            +
                except ValueError:
         | 
| 91 | 
            +
                    # Not an integer, continue...
         | 
| 92 | 
            +
                    pass
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                # 2. Try to convert to a float
         | 
| 95 | 
            +
                try:
         | 
| 96 | 
            +
                    return float(s)
         | 
| 97 | 
            +
                except ValueError:
         | 
| 98 | 
            +
                    # Not a float, continue...
         | 
| 99 | 
            +
                    pass
         | 
| 100 |  | 
| 101 | 
            +
                # 3. Check for a boolean value
         | 
| 102 | 
            +
                # This explicit check is important because bool('False') evaluates to True.
         | 
| 103 | 
            +
                s_lower = s.lower()
         | 
| 104 | 
            +
                if s_lower == 'true':
         | 
| 105 | 
            +
                    return True
         | 
| 106 | 
            +
                if s_lower == 'false':
         | 
| 107 | 
            +
                    return False
         | 
| 108 | 
            +
                    
         | 
| 109 | 
            +
                # 4. If nothing else worked, return the original string
         | 
| 110 | 
            +
                return s
         | 
| 111 |  | 
| 112 | 
             
            def handle_load_metadata(image_data: ImageMeta | None) -> List[Any]:
         | 
| 113 | 
             
                """
         | 
|  | |
| 127 | 
             
                raw_values = transfer_metadata(output_fields, metadata, dataclass_fields)
         | 
| 128 |  | 
| 129 | 
             
                output_values = [gr.skip()] * len(output_fields)
         | 
| 130 | 
            +
                for i, (component, value) in enumerate(zip(output_fields, raw_values)):        
         | 
| 131 | 
             
                    if hasattr(component, 'root_label'):
         | 
| 132 | 
             
                        output_values[i] = create_dataclass_instance(PropertyConfig, value)
         | 
| 133 | 
             
                    else:
         | 
| 134 | 
            +
                        output_values[i] = gr.update(value=infer_type(value))
         | 
| 135 |  | 
| 136 | 
             
                return output_values
         | 
| 137 |  | 
|  | |
| 151 |  | 
| 152 | 
             
                params = list(inputs)
         | 
| 153 | 
             
                image_params = dict(zip(input_fields.keys(), params))
         | 
| 154 | 
            +
                #dataclass_fields = build_dataclass_fields(PropertyConfig)
         | 
| 155 | 
            +
                metadata = {label: image_params.get(label, "") for label in image_params.keys()}
         | 
| 156 |  | 
| 157 | 
             
                new_filepath = output_dir / "image_with_meta.png"
         | 
| 158 | 
            +
                
         | 
| 159 | 
             
                add_metadata(image_data, metadata, new_filepath)
         | 
| 160 |  | 
| 161 | 
             
                return str(new_filepath)
         | 
|  | |
| 180 | 
             
                        label="Upload Image (Custom metadata only)",
         | 
| 181 | 
             
                        type="filepath",
         | 
| 182 | 
             
                        width=300,
         | 
| 183 | 
            +
                        height=400,            
         | 
|  | |
| 184 | 
             
                        interactive=True
         | 
| 185 | 
             
                    )
         | 
| 186 | 
             
                    img_all = ImageMeta(
         | 
| 187 | 
             
                        label="Upload Image (All metadata)",
         | 
| 188 | 
             
                        only_custom_metadata=False,
         | 
| 189 | 
            +
                        type="filepath",
         | 
| 190 | 
             
                        width=300,
         | 
| 191 | 
            +
                        height=400,            
         | 
| 192 | 
             
                        popup_metadata_height=400,
         | 
| 193 | 
            +
                        popup_metadata_width=500,
         | 
| 194 | 
            +
                        interactive=True
         | 
| 195 | 
             
                    )
         | 
| 196 |  | 
| 197 | 
             
                gr.Markdown("## Metadata Viewer")
         | 
|  | |
| 217 | 
             
                with gr.Row():
         | 
| 218 | 
             
                    save_button = gr.Button("Add Metadata and Save Image")
         | 
| 219 | 
             
                    saved_file_output = gr.File(label="Download Image")
         | 
| 220 | 
            +
               
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 221 |  | 
| 222 | 
             
                input_fields = {
         | 
| 223 | 
             
                    "Model": model_box,
         | 
    	
        src/backend/gradio_imagemeta/helpers.py
    CHANGED
    
    | @@ -107,6 +107,7 @@ def add_metadata(image_data: str | Path | Image.Image | np.ndarray, metadata: Di | |
| 107 | 
             
                    if not bool(save_path):
         | 
| 108 | 
             
                        return False
         | 
| 109 |  | 
|  | |
| 110 | 
             
                    # Convert image_data to PIL.Image
         | 
| 111 | 
             
                    if isinstance(image_data, (str, Path)):
         | 
| 112 | 
             
                        image = Image.open(image_data)
         | 
|  | |
| 107 | 
             
                    if not bool(save_path):
         | 
| 108 | 
             
                        return False
         | 
| 109 |  | 
| 110 | 
            +
                    
         | 
| 111 | 
             
                    # Convert image_data to PIL.Image
         | 
| 112 | 
             
                    if isinstance(image_data, (str, Path)):
         | 
| 113 | 
             
                        image = Image.open(image_data)
         | 
    	
        src/backend/gradio_imagemeta/templates/component/index.js
    CHANGED
    
    | The diff for this file is too large to render. 
		See raw diff | 
|  | 
    	
        src/backend/gradio_imagemeta/templates/component/style.css
    CHANGED
    
    | @@ -1 +1 @@ | |
| 1 | 
            -
            .block.svelte-239wnu{position:relative;margin:0;box-shadow:var(--block-shadow);border-width:var(--block-border-width);border-color:var(--block-border-color);border-radius:var(--block-radius);background:var(--block-background-fill);width:100%;line-height:var(--line-sm)}.block.fullscreen.svelte-239wnu{border-radius:0}.auto-margin.svelte-239wnu{margin-left:auto;margin-right:auto}.block.border_focus.svelte-239wnu{border-color:var(--color-accent)}.block.border_contrast.svelte-239wnu{border-color:var(--body-text-color)}.padded.svelte-239wnu{padding:var(--block-padding)}.hidden.svelte-239wnu{display:none}.flex.svelte-239wnu{display:flex;flex-direction:column}.hide-container.svelte-239wnu:not(.fullscreen){margin:0;box-shadow:none;--block-border-width:0;background:transparent;padding:0;overflow:visible}.resize-handle.svelte-239wnu{position:absolute;bottom:0;right:0;width:10px;height:10px;fill:var(--block-border-color);cursor:nwse-resize}.fullscreen.svelte-239wnu{position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:1000;overflow:auto}.animating.svelte-239wnu{animation:svelte-239wnu-pop-out .1s ease-out forwards}@keyframes svelte-239wnu-pop-out{0%{position:fixed;top:var(--start-top);left:var(--start-left);width:var(--start-width);height:var(--start-height);z-index:100}to{position:fixed;top:0vh;left:0vw;width:100vw;height:100vh;z-index:1000}}.placeholder.svelte-239wnu{border-radius:var(--block-radius);border-width:var(--block-border-width);border-color:var(--block-border-color);border-style:dashed}Tables */ table,tr,td,th{margin-top:var(--spacing-sm);margin-bottom:var(--spacing-sm);padding:var(--spacing-xl)}.md code,.md pre{background:none;font-family:var(--font-mono);font-size:var(--text-sm);text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:2;tab-size:2;-webkit-hyphens:none;hyphens:none}.md pre[class*=language-]::selection,.md pre[class*=language-] ::selection,.md code[class*=language-]::selection,.md code[class*=language-] ::selection{text-shadow:none;background:#b3d4fc}.md pre{padding:1em;margin:.5em 0;overflow:auto;position:relative;margin-top:var(--spacing-sm);margin-bottom:var(--spacing-sm);box-shadow:none;border:none;border-radius:var(--radius-md);background:var(--code-background-fill);padding:var(--spacing-xxl);font-family:var(--font-mono);text-shadow:none;border-radius:var(--radius-sm);white-space:nowrap;display:block;white-space:pre}.md :not(pre)>code{padding:.1em;border-radius:var(--radius-xs);white-space:normal;background:var(--code-background-fill);border:1px solid var(--panel-border-color);padding:var(--spacing-xxs) var(--spacing-xs)}.md .token.comment,.md .token.prolog,.md .token.doctype,.md .token.cdata{color:#708090}.md .token.punctuation{color:#999}.md .token.namespace{opacity:.7}.md .token.property,.md .token.tag,.md .token.boolean,.md .token.number,.md .token.constant,.md .token.symbol,.md .token.deleted{color:#905}.md .token.selector,.md .token.attr-name,.md .token.string,.md .token.char,.md .token.builtin,.md .token.inserted{color:#690}.md .token.atrule,.md .token.attr-value,.md .token.keyword{color:#07a}.md .token.function,.md .token.class-name{color:#dd4a68}.md .token.regex,.md .token.important,.md .token.variable{color:#e90}.md .token.important,.md .token.bold{font-weight:700}.md .token.italic{font-style:italic}.md .token.entity{cursor:help}.dark .md .token.comment,.dark .md .token.prolog,.dark .md .token.cdata{color:#5c6370}.dark .md .token.doctype,.dark .md .token.punctuation,.dark .md .token.entity{color:#abb2bf}.dark .md .token.attr-name,.dark .md .token.class-name,.dark .md .token.boolean,.dark .md .token.constant,.dark .md .token.number,.dark .md .token.atrule{color:#d19a66}.dark .md .token.keyword{color:#c678dd}.dark .md .token.property,.dark .md .token.tag,.dark .md .token.symbol,.dark .md .token.deleted,.dark .md .token.important{color:#e06c75}.dark .md .token.selector,.dark .md .token.string,.dark .md .token.char,.dark .md .token.builtin,.dark .md .token.inserted,.dark .md .token.regex,.dark .md .token.attr-value,.dark .md .token.attr-value>.token.punctuation{color:#98c379}.dark .md .token.variable,.dark .md .token.operator,.dark .md .token.function{color:#61afef}.dark .md .token.url{color:#56b6c2}span.svelte-1m32c2s div[class*=code_wrap]{position:relative}span.svelte-1m32c2s span.katex{font-size:var(--text-lg);direction:ltr}span.svelte-1m32c2s div[class*=code_wrap]>button{z-index:1;cursor:pointer;border-bottom-left-radius:var(--radius-sm);padding:var(--spacing-md);width:25px;height:25px;position:absolute;right:0}span.svelte-1m32c2s .check{opacity:0;z-index:var(--layer-top);transition:opacity .2s;background:var(--code-background-fill);color:var(--body-text-color);position:absolute;top:var(--size-1-5);left:var(--size-1-5)}span.svelte-1m32c2s p:not(:first-child){margin-top:var(--spacing-xxl)}span.svelte-1m32c2s .md-header-anchor{margin-left:-25px;padding-right:8px;line-height:1;color:var(--body-text-color-subdued);opacity:0}span.svelte-1m32c2s h1:hover .md-header-anchor,span.svelte-1m32c2s h2:hover .md-header-anchor,span.svelte-1m32c2s h3:hover .md-header-anchor,span.svelte-1m32c2s h4:hover .md-header-anchor,span.svelte-1m32c2s h5:hover .md-header-anchor,span.svelte-1m32c2s h6:hover .md-header-anchor{opacity:1}span.md.svelte-1m32c2s .md-header-anchor>svg{color:var(--body-text-color-subdued)}span.svelte-1m32c2s table{word-break:break-word}div.svelte-17qq50w>.md.prose{font-weight:var(--block-info-text-weight);font-size:var(--block-info-text-size);line-height:var(--line-sm)}div.svelte-17qq50w>.md.prose *{color:var(--block-info-text-color)}div.svelte-17qq50w{margin-bottom:var(--spacing-md)}span.has-info.svelte-zgrq3{margin-bottom:var(--spacing-xs)}span.svelte-zgrq3:not(.has-info){margin-bottom:var(--spacing-lg)}span.svelte-zgrq3{display:inline-block;position:relative;z-index:var(--layer-4);border:solid var(--block-title-border-width) var(--block-title-border-color);border-radius:var(--block-title-radius);background:var(--block-title-background-fill);padding:var(--block-title-padding);color:var(--block-title-text-color);font-weight:var(--block-title-text-weight);font-size:var(--block-title-text-size);line-height:var(--line-sm)}span[dir=rtl].svelte-zgrq3{display:block}.hide.svelte-zgrq3{margin:0;height:0}label.svelte-13ao5pu.svelte-13ao5pu{display:inline-flex;align-items:center;z-index:var(--layer-2);box-shadow:var(--block-label-shadow);border:var(--block-label-border-width) solid var(--block-label-border-color);border-top:none;border-left:none;border-radius:var(--block-label-radius);background:var(--block-label-background-fill);padding:var(--block-label-padding);pointer-events:none;color:var(--block-label-text-color);font-weight:var(--block-label-text-weight);font-size:var(--block-label-text-size);line-height:var(--line-sm)}.gr-group label.svelte-13ao5pu.svelte-13ao5pu{border-top-left-radius:0}label.float.svelte-13ao5pu.svelte-13ao5pu{position:absolute;top:var(--block-label-margin);left:var(--block-label-margin)}label.svelte-13ao5pu.svelte-13ao5pu:not(.float){position:static;margin-top:var(--block-label-margin);margin-left:var(--block-label-margin)}.hide.svelte-13ao5pu.svelte-13ao5pu{height:0}span.svelte-13ao5pu.svelte-13ao5pu{opacity:.8;margin-right:var(--size-2);width:calc(var(--block-label-text-size) - 1px);height:calc(var(--block-label-text-size) - 1px)}.hide-label.svelte-13ao5pu.svelte-13ao5pu{box-shadow:none;border-width:0;background:transparent;overflow:visible}label[dir=rtl].svelte-13ao5pu.svelte-13ao5pu{border:var(--block-label-border-width) solid var(--block-label-border-color);border-top:none;border-right:none;border-bottom-left-radius:var(--block-radius);border-bottom-right-radius:var(--block-label-radius);border-top-left-radius:var(--block-label-radius)}label[dir=rtl].svelte-13ao5pu span.svelte-13ao5pu{margin-left:var(--size-2);margin-right:0}button.svelte-qgco6m{display:flex;justify-content:center;align-items:center;gap:1px;z-index:var(--layer-2);border-radius:var(--radius-xs);color:var(--block-label-text-color);border:1px solid transparent;padding:var(--spacing-xxs)}button.svelte-qgco6m:hover{background-color:var(--background-fill-secondary)}button[disabled].svelte-qgco6m{opacity:.5;box-shadow:none}button[disabled].svelte-qgco6m:hover{cursor:not-allowed}.padded.svelte-qgco6m{background:var(--bg-color)}button.svelte-qgco6m:hover,button.highlight.svelte-qgco6m{cursor:pointer;color:var(--color-accent)}.padded.svelte-qgco6m:hover{color:var(--block-label-text-color)}span.svelte-qgco6m{padding:0 1px;font-size:10px}div.svelte-qgco6m{display:flex;align-items:center;justify-content:center;transition:filter .2s ease-in-out}.x-small.svelte-qgco6m{width:10px;height:10px}.small.svelte-qgco6m{width:14px;height:14px}.medium.svelte-qgco6m{width:20px;height:20px}.large.svelte-qgco6m{width:22px;height:22px}.pending.svelte-qgco6m{animation:svelte-qgco6m-flash .5s infinite}@keyframes svelte-qgco6m-flash{0%{opacity:.5}50%{opacity:1}to{opacity:.5}}.transparent.svelte-qgco6m{background:transparent;border:none;box-shadow:none}.empty.svelte-3w3rth{display:flex;justify-content:center;align-items:center;margin-top:calc(0px - var(--size-6));height:var(--size-full)}.icon.svelte-3w3rth{opacity:.5;height:var(--size-5);color:var(--body-text-color)}.small.svelte-3w3rth{min-height:calc(var(--size-32) - 20px)}.large.svelte-3w3rth{min-height:calc(var(--size-64) - 20px)}.unpadded_box.svelte-3w3rth{margin-top:0}.small_parent.svelte-3w3rth{min-height:100%!important}.dropdown-arrow.svelte-145leq6,.dropdown-arrow.svelte-ihhdbf{fill:currentColor}.circle.svelte-ihhdbf{fill:currentColor;opacity:.1}svg.svelte-pb9pol{animation:svelte-pb9pol-spin 1.5s linear infinite}@keyframes svelte-pb9pol-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}h2.svelte-1xg7h5n{font-size:var(--text-xl)!important}p.svelte-1xg7h5n,h2.svelte-1xg7h5n{white-space:pre-line}.wrap.svelte-1xg7h5n{display:flex;flex-direction:column;justify-content:center;align-items:center;min-height:var(--size-60);color:var(--block-label-text-color);line-height:var(--line-md);height:100%;padding-top:var(--size-3);text-align:center;margin:auto var(--spacing-lg)}.or.svelte-1xg7h5n{color:var(--body-text-color-subdued);display:flex}.icon-wrap.svelte-1xg7h5n{width:30px;margin-bottom:var(--spacing-lg)}@media (--screen-md){.wrap.svelte-1xg7h5n{font-size:var(--text-lg)}}.hovered.svelte-1xg7h5n{color:var(--color-accent)}div.svelte-q32hvf{border-top:1px solid transparent;display:flex;max-height:100%;justify-content:center;align-items:center;gap:var(--spacing-sm);height:auto;align-items:flex-end;color:var(--block-label-text-color);flex-shrink:0}.show_border.svelte-q32hvf{border-top:1px solid var(--block-border-color);margin-top:var(--spacing-xxl);box-shadow:var(--shadow-drop)}.source-selection.svelte-15ls1gu{display:flex;align-items:center;justify-content:center;border-top:1px solid var(--border-color-primary);width:100%;margin-left:auto;margin-right:auto;height:var(--size-10)}.icon.svelte-15ls1gu{width:22px;height:22px;margin:var(--spacing-lg) var(--spacing-xs);padding:var(--spacing-xs);color:var(--neutral-400);border-radius:var(--radius-md)}.selected.svelte-15ls1gu{color:var(--color-accent)}.icon.svelte-15ls1gu:hover,.icon.svelte-15ls1gu:focus{color:var(--color-accent)}.icon-button-wrapper.svelte-109se4{display:flex;flex-direction:row;align-items:center;justify-content:center;z-index:var(--layer-3);gap:var(--spacing-sm);box-shadow:var(--shadow-drop);border:1px solid var(--border-color-primary);background:var(--block-background-fill);padding:var(--spacing-xxs)}.icon-button-wrapper.hide-top-corner.svelte-109se4{border-top:none;border-right:none;border-radius:var(--block-label-right-radius)}.icon-button-wrapper.display-top-corner.svelte-109se4{border-radius:var(--radius-sm) 0 0 var(--radius-sm);top:var(--spacing-sm);right:-1px}.icon-button-wrapper.svelte-109se4:not(.top-panel){border:1px solid var(--border-color-primary);border-radius:var(--radius-sm)}.top-panel.svelte-109se4{position:absolute;top:var(--block-label-margin);right:var(--block-label-margin);margin:0}.icon-button-wrapper.svelte-109se4 button{margin:var(--spacing-xxs);border-radius:var(--radius-xs);position:relative}.icon-button-wrapper.svelte-109se4 a.download-link:not(:last-child),.icon-button-wrapper.svelte-109se4 button:not(:last-child){margin-right:var(--spacing-xxs)}.icon-button-wrapper.svelte-109se4 a.download-link:not(:last-child):not(.no-border *):after,.icon-button-wrapper.svelte-109se4 button:not(:last-child):not(.no-border *):after{content:"";position:absolute;right:-4.5px;top:15%;height:70%;width:1px;background-color:var(--border-color-primary)}.icon-button-wrapper.svelte-109se4>*{height:100%}.unstyled-link.svelte-151nsdd{all:unset;cursor:pointer}img.svelte-kxeri3{object-fit:cover}.image-container.svelte-157jyrf.svelte-157jyrf{height:100%;position:relative;min-width:var(--size-20)}.image-container.svelte-157jyrf button.svelte-157jyrf{width:var(--size-full);height:var(--size-full);border-radius:var(--radius-lg);display:flex;align-items:center;justify-content:center}.image-frame.svelte-157jyrf img{width:var(--size-full);height:var(--size-full);object-fit:scale-down}.selectable.svelte-157jyrf.svelte-157jyrf{cursor:crosshair}.fullscreen-controls svg{position:relative;top:0}.image-container:fullscreen{background-color:#000;display:flex;justify-content:center;align-items:center}.image-container:fullscreen img{max-width:90vw;max-height:90vh;object-fit:scale-down}.image-frame.svelte-157jyrf.svelte-157jyrf{width:auto;height:100%;display:flex;align-items:center;justify-content:center}.metadata-popup.svelte-157jyrf.svelte-157jyrf{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--background-fill-primary, white);border:1px solid var(--border-color-primary);box-shadow:0 4px 8px #0003;z-index:1000;border-radius:8px;overflow:hidden}.popup-content.svelte-157jyrf.svelte-157jyrf{position:relative;padding:1rem;display:flex;flex-direction:column;height:100%}.popup-title.svelte-157jyrf.svelte-157jyrf{font-weight:700;margin:0 0 1rem}.close-button.svelte-157jyrf.svelte-157jyrf{position:absolute;top:.5rem;right:.5rem;background:none;border:none;font-size:1rem;cursor:pointer}.metadata-table-container.svelte-157jyrf.svelte-157jyrf{flex:1;overflow:auto}.metadata-table.svelte-157jyrf.svelte-157jyrf{width:100%;border-collapse:collapse}.metadata-label.svelte-157jyrf.svelte-157jyrf{background:var(--background-fill-secondary, #f5f5f5);padding:.5rem;font-weight:700;text-align:left;vertical-align:top;width:40%}.metadata-value.svelte-157jyrf.svelte-157jyrf{padding:.5rem;white-space:nowrap;vertical-align:top}.load-metadata-button.svelte-157jyrf.svelte-157jyrf{margin-top:1rem;padding:.5rem 1rem;background-color:var(--button-primary-background-fill);color:var(--button-primary-text-color);border:none;border-radius:4px;cursor:pointer;align-self:center}.load-metadata-button.svelte-157jyrf.svelte-157jyrf:hover{background-color:var(--button-primary-background-fill-hover)}.wrap.svelte-cr2edf.svelte-cr2edf{overflow-y:auto;transition:opacity .5s ease-in-out;background:var(--block-background-fill);position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:var(--size-40);width:var(--size-full)}.wrap.svelte-cr2edf.svelte-cr2edf:after{content:"";position:absolute;top:0;left:0;width:var(--upload-progress-width);height:100%;transition:all .5s ease-in-out;z-index:1}.uploading.svelte-cr2edf.svelte-cr2edf{font-size:var(--text-lg);font-family:var(--font);z-index:2}.file-name.svelte-cr2edf.svelte-cr2edf{margin:var(--spacing-md);font-size:var(--text-lg);color:var(--body-text-color-subdued)}.file.svelte-cr2edf.svelte-cr2edf{font-size:var(--text-md);z-index:2;display:flex;align-items:center}.file.svelte-cr2edf progress.svelte-cr2edf{display:inline;height:var(--size-1);width:100%;transition:all .5s ease-in-out;color:var(--color-accent);border:none}.file.svelte-cr2edf progress[value].svelte-cr2edf::-webkit-progress-value{background-color:var(--color-accent);border-radius:20px}.file.svelte-cr2edf progress[value].svelte-cr2edf::-webkit-progress-bar{background-color:var(--border-color-accent);border-radius:20px}.progress-bar.svelte-cr2edf.svelte-cr2edf{width:14px;height:14px;border-radius:50%;background:radial-gradient(closest-side,var(--block-background-fill) 64%,transparent 53% 100%),conic-gradient(var(--color-accent) var(--upload-progress-width),var(--border-color-accent) 0);transition:all .5s ease-in-out}button.svelte-1o7nwih{cursor:pointer;width:var(--size-full)}.center.svelte-1o7nwih{display:flex;justify-content:center}.flex.svelte-1o7nwih{display:flex;flex-direction:column;justify-content:center;align-items:center}.hidden.svelte-1o7nwih{display:none;position:absolute;flex-grow:0}.hidden.svelte-1o7nwih svg{display:none}.disable_click.svelte-1o7nwih{cursor:default}.icon-mode.svelte-1o7nwih{position:absolute!important;width:var(--size-4);height:var(--size-4);padding:0;min-height:0;border-radius:var(--radius-circle)}.icon-mode.svelte-1o7nwih svg{width:var(--size-4);height:var(--size-4)}.image-frame.svelte-16v1i9j img{width:var(--size-full);height:var(--size-full);object-fit:scale-down}.upload-container.svelte-16v1i9j{display:flex;align-items:center;justify-content:center;height:100%;flex-shrink:1;max-height:100%}.image-container.svelte-16v1i9j{display:flex;height:100%;flex-direction:column;justify-content:center;align-items:center;max-height:100%;position:relative}.selectable.svelte-16v1i9j{cursor:crosshair}.image-frame.svelte-16v1i9j{object-fit:cover;width:100%;height:100%}.metadata-popup.svelte-16v1i9j{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--background-fill-primary, white);border:1px solid var(--border-color-primary);box-shadow:0 4px 8px #0003;z-index:1000;border-radius:8px;overflow:hidden}.popup-content.svelte-16v1i9j{position:relative;padding:1rem;display:flex;flex-direction:column;height:100%}.popup-title.svelte-16v1i9j{font-weight:700;margin:0 0 1rem}.close-button.svelte-16v1i9j{position:absolute;top:.5rem;right:.5rem;background:none;border:none;font-size:1rem;cursor:pointer}.metadata-table-container.svelte-16v1i9j{flex:1;overflow:auto}.metadata-table.svelte-16v1i9j{width:100%;border-collapse:collapse}.metadata-label.svelte-16v1i9j{background:var(--background-fill-secondary, #f5f5f5);padding:.5rem;font-weight:700;text-align:left;vertical-align:top;width:40%}.metadata-value.svelte-16v1i9j{padding:.5rem;white-space:nowrap;vertical-align:top}.load-metadata-button.svelte-16v1i9j{margin-top:1rem;padding:.5rem 1rem;background-color:var(--button-primary-background-fill);color:var(--button-primary-text-color);border:none;border-radius:4px;cursor:pointer;align-self:center}.load-metadata-button.svelte-16v1i9j:hover{background-color:var(--button-primary-background-fill-hover)}svg.svelte-43sxxs.svelte-43sxxs{width:var(--size-20);height:var(--size-20)}svg.svelte-43sxxs path.svelte-43sxxs{fill:var(--loader-color)}div.svelte-43sxxs.svelte-43sxxs{z-index:var(--layer-2)}.margin.svelte-43sxxs.svelte-43sxxs{margin:var(--size-4)}.wrap.svelte-17v219f.svelte-17v219f{display:flex;flex-direction:column;justify-content:center;align-items:center;z-index:var(--layer-2);transition:opacity .1s ease-in-out;border-radius:var(--block-radius);background:var(--block-background-fill);padding:0 var(--size-6);max-height:var(--size-screen-h);overflow:hidden}.wrap.center.svelte-17v219f.svelte-17v219f{top:0;right:0;left:0}.wrap.default.svelte-17v219f.svelte-17v219f{top:0;right:0;bottom:0;left:0}.hide.svelte-17v219f.svelte-17v219f{opacity:0;pointer-events:none}.generating.svelte-17v219f.svelte-17v219f{animation:svelte-17v219f-pulseStart 1s cubic-bezier(.4,0,.6,1),svelte-17v219f-pulse 2s cubic-bezier(.4,0,.6,1) 1s infinite;border:2px solid var(--color-accent);background:transparent;z-index:var(--layer-1);pointer-events:none}.translucent.svelte-17v219f.svelte-17v219f{background:none}@keyframes svelte-17v219f-pulseStart{0%{opacity:0}to{opacity:1}}@keyframes svelte-17v219f-pulse{0%,to{opacity:1}50%{opacity:.5}}.loading.svelte-17v219f.svelte-17v219f{z-index:var(--layer-2);color:var(--body-text-color)}.eta-bar.svelte-17v219f.svelte-17v219f{position:absolute;top:0;right:0;bottom:0;left:0;transform-origin:left;opacity:.8;z-index:var(--layer-1);transition:10ms;background:var(--background-fill-secondary)}.progress-bar-wrap.svelte-17v219f.svelte-17v219f{border:1px solid var(--border-color-primary);background:var(--background-fill-primary);width:55.5%;height:var(--size-4)}.progress-bar.svelte-17v219f.svelte-17v219f{transform-origin:left;background-color:var(--loader-color);width:var(--size-full);height:var(--size-full)}.progress-level.svelte-17v219f.svelte-17v219f{display:flex;flex-direction:column;align-items:center;gap:1;z-index:var(--layer-2);width:var(--size-full)}.progress-level-inner.svelte-17v219f.svelte-17v219f{margin:var(--size-2) auto;color:var(--body-text-color);font-size:var(--text-sm);font-family:var(--font-mono)}.meta-text.svelte-17v219f.svelte-17v219f{position:absolute;bottom:0;right:0;z-index:var(--layer-2);padding:var(--size-1) var(--size-2);font-size:var(--text-sm);font-family:var(--font-mono)}.meta-text-center.svelte-17v219f.svelte-17v219f{display:flex;position:absolute;top:0;right:0;justify-content:center;align-items:center;transform:translateY(var(--size-6));z-index:var(--layer-2);padding:var(--size-1) var(--size-2);font-size:var(--text-sm);font-family:var(--font-mono);text-align:center}.error.svelte-17v219f.svelte-17v219f{box-shadow:var(--shadow-drop);border:solid 1px var(--error-border-color);border-radius:var(--radius-full);background:var(--error-background-fill);padding-right:var(--size-4);padding-left:var(--size-4);color:var(--error-text-color);font-weight:var(--weight-semibold);font-size:var(--text-lg);line-height:var(--line-lg);font-family:var(--font)}.minimal.svelte-17v219f.svelte-17v219f{pointer-events:none}.minimal.svelte-17v219f .progress-text.svelte-17v219f{background:var(--block-background-fill)}.border.svelte-17v219f.svelte-17v219f{border:1px solid var(--border-color-primary)}.clear-status.svelte-17v219f.svelte-17v219f{position:absolute;display:flex;top:var(--size-2);right:var(--size-2);justify-content:flex-end;gap:var(--spacing-sm);z-index:var(--layer-1)}.toast-body.svelte-syezpc{display:flex;position:relative;right:0;left:0;align-items:center;margin:var(--size-6) var(--size-4);margin:auto;border-radius:var(--container-radius);overflow:hidden;pointer-events:auto}.toast-body.error.svelte-syezpc{border:1px solid var(--color-red-700);background:var(--color-red-50)}.dark .toast-body.error.svelte-syezpc{border:1px solid var(--color-red-500);background-color:var(--color-grey-950)}.toast-body.warning.svelte-syezpc{border:1px solid var(--color-yellow-700);background:var(--color-yellow-50)}.dark .toast-body.warning.svelte-syezpc{border:1px solid var(--color-yellow-500);background-color:var(--color-grey-950)}.toast-body.info.svelte-syezpc{border:1px solid var(--color-grey-700);background:var(--color-grey-50)}.dark .toast-body.info.svelte-syezpc{border:1px solid var(--color-grey-500);background-color:var(--color-grey-950)}.toast-body.success.svelte-syezpc{border:1px solid var(--color-green-700);background:var(--color-green-50)}.dark .toast-body.success.svelte-syezpc{border:1px solid var(--color-green-500);background-color:var(--color-grey-950)}.toast-title.svelte-syezpc{display:flex;align-items:center;font-weight:var(--weight-bold);font-size:var(--text-lg);line-height:var(--line-sm)}.toast-title.error.svelte-syezpc{color:var(--color-red-700)}.dark .toast-title.error.svelte-syezpc{color:var(--color-red-50)}.toast-title.warning.svelte-syezpc{color:var(--color-yellow-700)}.dark .toast-title.warning.svelte-syezpc{color:var(--color-yellow-50)}.toast-title.info.svelte-syezpc{color:var(--color-grey-700)}.dark .toast-title.info.svelte-syezpc{color:var(--color-grey-50)}.toast-title.success.svelte-syezpc{color:var(--color-green-700)}.dark .toast-title.success.svelte-syezpc{color:var(--color-green-50)}.toast-close.svelte-syezpc{margin:0 var(--size-3);border-radius:var(--size-3);padding:0px var(--size-1-5);font-size:var(--size-5);line-height:var(--size-5)}.toast-close.error.svelte-syezpc{color:var(--color-red-700)}.dark .toast-close.error.svelte-syezpc{color:var(--color-red-500)}.toast-close.warning.svelte-syezpc{color:var(--color-yellow-700)}.dark .toast-close.warning.svelte-syezpc{color:var(--color-yellow-500)}.toast-close.info.svelte-syezpc{color:var(--color-grey-700)}.dark .toast-close.info.svelte-syezpc{color:var(--color-grey-500)}.toast-close.success.svelte-syezpc{color:var(--color-green-700)}.dark .toast-close.success.svelte-syezpc{color:var(--color-green-500)}.toast-text.svelte-syezpc{font-size:var(--text-lg);word-wrap:break-word;overflow-wrap:break-word;word-break:break-word}.toast-text.error.svelte-syezpc{color:var(--color-red-700)}.dark .toast-text.error.svelte-syezpc{color:var(--color-red-50)}.toast-text.warning.svelte-syezpc{color:var(--color-yellow-700)}.dark .toast-text.warning.svelte-syezpc{color:var(--color-yellow-50)}.toast-text.info.svelte-syezpc{color:var(--color-grey-700)}.dark .toast-text.info.svelte-syezpc{color:var(--color-grey-50)}.toast-text.success.svelte-syezpc{color:var(--color-green-700)}.dark .toast-text.success.svelte-syezpc{color:var(--color-green-50)}.toast-details.svelte-syezpc{margin:var(--size-3) var(--size-3) var(--size-3) 0;width:100%}.toast-icon.svelte-syezpc{display:flex;position:absolute;position:relative;flex-shrink:0;justify-content:center;align-items:center;margin:var(--size-2);border-radius:var(--radius-full);padding:var(--size-1);padding-left:calc(var(--size-1) - 1px);width:35px;height:35px}.toast-icon.error.svelte-syezpc{color:var(--color-red-700)}.dark .toast-icon.error.svelte-syezpc{color:var(--color-red-500)}.toast-icon.warning.svelte-syezpc{color:var(--color-yellow-700)}.dark .toast-icon.warning.svelte-syezpc{color:var(--color-yellow-500)}.toast-icon.info.svelte-syezpc{color:var(--color-grey-700)}.dark .toast-icon.info.svelte-syezpc{color:var(--color-grey-500)}.toast-icon.success.svelte-syezpc{color:var(--color-green-700)}.dark .toast-icon.success.svelte-syezpc{color:var(--color-green-500)}@keyframes svelte-syezpc-countdown{0%{transform:scaleX(1)}to{transform:scaleX(0)}}.timer.svelte-syezpc{position:absolute;bottom:0;left:0;transform-origin:0 0;animation:svelte-syezpc-countdown 10s linear forwards;width:100%;height:var(--size-1)}.timer.error.svelte-syezpc{background:var(--color-red-700)}.dark .timer.error.svelte-syezpc{background:var(--color-red-500)}.timer.warning.svelte-syezpc{background:var(--color-yellow-700)}.dark .timer.warning.svelte-syezpc{background:var(--color-yellow-500)}.timer.info.svelte-syezpc{background:var(--color-grey-700)}.dark .timer.info.svelte-syezpc{background:var(--color-grey-500)}.timer.success.svelte-syezpc{background:var(--color-green-700)}.dark .timer.success.svelte-syezpc{background:var(--color-green-500)}.hidden.svelte-syezpc{display:none}.toast-text.svelte-syezpc a{text-decoration:underline}.toast-wrap.svelte-gatr8h{display:flex;position:fixed;top:var(--size-4);right:var(--size-4);flex-direction:column;align-items:end;gap:var(--size-2);z-index:var(--layer-top);width:calc(100% - var(--size-8))}@media (--screen-sm){.toast-wrap.svelte-gatr8h{width:calc(var(--size-96) + var(--size-10))}}.streaming-bar.svelte-ga0jj6{position:absolute;bottom:0;left:0;right:0;height:4px;background-color:var(--primary-600);animation:svelte-ga0jj6-countdown linear forwards;z-index:1}@keyframes svelte-ga0jj6-countdown{0%{transform:translate(0)}to{transform:translate(-100%)}}.container.svelte-1sgcyba img{width:100%;height:100%}.container.selected.svelte-1sgcyba{border-color:var(--border-color-accent)}.border.table.svelte-1sgcyba{border:2px solid var(--border-color-primary)}.container.table.svelte-1sgcyba{margin:0 auto;border-radius:var(--radius-lg);overflow:hidden;width:var(--size-20);height:var(--size-20);object-fit:cover}.container.gallery.svelte-1sgcyba{width:var(--size-20);max-width:var(--size-20);object-fit:cover}
         | 
|  | |
| 1 | 
            +
            .block.svelte-239wnu{position:relative;margin:0;box-shadow:var(--block-shadow);border-width:var(--block-border-width);border-color:var(--block-border-color);border-radius:var(--block-radius);background:var(--block-background-fill);width:100%;line-height:var(--line-sm)}.block.fullscreen.svelte-239wnu{border-radius:0}.auto-margin.svelte-239wnu{margin-left:auto;margin-right:auto}.block.border_focus.svelte-239wnu{border-color:var(--color-accent)}.block.border_contrast.svelte-239wnu{border-color:var(--body-text-color)}.padded.svelte-239wnu{padding:var(--block-padding)}.hidden.svelte-239wnu{display:none}.flex.svelte-239wnu{display:flex;flex-direction:column}.hide-container.svelte-239wnu:not(.fullscreen){margin:0;box-shadow:none;--block-border-width:0;background:transparent;padding:0;overflow:visible}.resize-handle.svelte-239wnu{position:absolute;bottom:0;right:0;width:10px;height:10px;fill:var(--block-border-color);cursor:nwse-resize}.fullscreen.svelte-239wnu{position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:1000;overflow:auto}.animating.svelte-239wnu{animation:svelte-239wnu-pop-out .1s ease-out forwards}@keyframes svelte-239wnu-pop-out{0%{position:fixed;top:var(--start-top);left:var(--start-left);width:var(--start-width);height:var(--start-height);z-index:100}to{position:fixed;top:0vh;left:0vw;width:100vw;height:100vh;z-index:1000}}.placeholder.svelte-239wnu{border-radius:var(--block-radius);border-width:var(--block-border-width);border-color:var(--block-border-color);border-style:dashed}Tables */ table,tr,td,th{margin-top:var(--spacing-sm);margin-bottom:var(--spacing-sm);padding:var(--spacing-xl)}.md code,.md pre{background:none;font-family:var(--font-mono);font-size:var(--text-sm);text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:2;tab-size:2;-webkit-hyphens:none;hyphens:none}.md pre[class*=language-]::selection,.md pre[class*=language-] ::selection,.md code[class*=language-]::selection,.md code[class*=language-] ::selection{text-shadow:none;background:#b3d4fc}.md pre{padding:1em;margin:.5em 0;overflow:auto;position:relative;margin-top:var(--spacing-sm);margin-bottom:var(--spacing-sm);box-shadow:none;border:none;border-radius:var(--radius-md);background:var(--code-background-fill);padding:var(--spacing-xxl);font-family:var(--font-mono);text-shadow:none;border-radius:var(--radius-sm);white-space:nowrap;display:block;white-space:pre}.md :not(pre)>code{padding:.1em;border-radius:var(--radius-xs);white-space:normal;background:var(--code-background-fill);border:1px solid var(--panel-border-color);padding:var(--spacing-xxs) var(--spacing-xs)}.md .token.comment,.md .token.prolog,.md .token.doctype,.md .token.cdata{color:#708090}.md .token.punctuation{color:#999}.md .token.namespace{opacity:.7}.md .token.property,.md .token.tag,.md .token.boolean,.md .token.number,.md .token.constant,.md .token.symbol,.md .token.deleted{color:#905}.md .token.selector,.md .token.attr-name,.md .token.string,.md .token.char,.md .token.builtin,.md .token.inserted{color:#690}.md .token.atrule,.md .token.attr-value,.md .token.keyword{color:#07a}.md .token.function,.md .token.class-name{color:#dd4a68}.md .token.regex,.md .token.important,.md .token.variable{color:#e90}.md .token.important,.md .token.bold{font-weight:700}.md .token.italic{font-style:italic}.md .token.entity{cursor:help}.dark .md .token.comment,.dark .md .token.prolog,.dark .md .token.cdata{color:#5c6370}.dark .md .token.doctype,.dark .md .token.punctuation,.dark .md .token.entity{color:#abb2bf}.dark .md .token.attr-name,.dark .md .token.class-name,.dark .md .token.boolean,.dark .md .token.constant,.dark .md .token.number,.dark .md .token.atrule{color:#d19a66}.dark .md .token.keyword{color:#c678dd}.dark .md .token.property,.dark .md .token.tag,.dark .md .token.symbol,.dark .md .token.deleted,.dark .md .token.important{color:#e06c75}.dark .md .token.selector,.dark .md .token.string,.dark .md .token.char,.dark .md .token.builtin,.dark .md .token.inserted,.dark .md .token.regex,.dark .md .token.attr-value,.dark .md .token.attr-value>.token.punctuation{color:#98c379}.dark .md .token.variable,.dark .md .token.operator,.dark .md .token.function{color:#61afef}.dark .md .token.url{color:#56b6c2}span.svelte-1m32c2s div[class*=code_wrap]{position:relative}span.svelte-1m32c2s span.katex{font-size:var(--text-lg);direction:ltr}span.svelte-1m32c2s div[class*=code_wrap]>button{z-index:1;cursor:pointer;border-bottom-left-radius:var(--radius-sm);padding:var(--spacing-md);width:25px;height:25px;position:absolute;right:0}span.svelte-1m32c2s .check{opacity:0;z-index:var(--layer-top);transition:opacity .2s;background:var(--code-background-fill);color:var(--body-text-color);position:absolute;top:var(--size-1-5);left:var(--size-1-5)}span.svelte-1m32c2s p:not(:first-child){margin-top:var(--spacing-xxl)}span.svelte-1m32c2s .md-header-anchor{margin-left:-25px;padding-right:8px;line-height:1;color:var(--body-text-color-subdued);opacity:0}span.svelte-1m32c2s h1:hover .md-header-anchor,span.svelte-1m32c2s h2:hover .md-header-anchor,span.svelte-1m32c2s h3:hover .md-header-anchor,span.svelte-1m32c2s h4:hover .md-header-anchor,span.svelte-1m32c2s h5:hover .md-header-anchor,span.svelte-1m32c2s h6:hover .md-header-anchor{opacity:1}span.md.svelte-1m32c2s .md-header-anchor>svg{color:var(--body-text-color-subdued)}span.svelte-1m32c2s table{word-break:break-word}div.svelte-17qq50w>.md.prose{font-weight:var(--block-info-text-weight);font-size:var(--block-info-text-size);line-height:var(--line-sm)}div.svelte-17qq50w>.md.prose *{color:var(--block-info-text-color)}div.svelte-17qq50w{margin-bottom:var(--spacing-md)}span.has-info.svelte-zgrq3{margin-bottom:var(--spacing-xs)}span.svelte-zgrq3:not(.has-info){margin-bottom:var(--spacing-lg)}span.svelte-zgrq3{display:inline-block;position:relative;z-index:var(--layer-4);border:solid var(--block-title-border-width) var(--block-title-border-color);border-radius:var(--block-title-radius);background:var(--block-title-background-fill);padding:var(--block-title-padding);color:var(--block-title-text-color);font-weight:var(--block-title-text-weight);font-size:var(--block-title-text-size);line-height:var(--line-sm)}span[dir=rtl].svelte-zgrq3{display:block}.hide.svelte-zgrq3{margin:0;height:0}label.svelte-13ao5pu.svelte-13ao5pu{display:inline-flex;align-items:center;z-index:var(--layer-2);box-shadow:var(--block-label-shadow);border:var(--block-label-border-width) solid var(--block-label-border-color);border-top:none;border-left:none;border-radius:var(--block-label-radius);background:var(--block-label-background-fill);padding:var(--block-label-padding);pointer-events:none;color:var(--block-label-text-color);font-weight:var(--block-label-text-weight);font-size:var(--block-label-text-size);line-height:var(--line-sm)}.gr-group label.svelte-13ao5pu.svelte-13ao5pu{border-top-left-radius:0}label.float.svelte-13ao5pu.svelte-13ao5pu{position:absolute;top:var(--block-label-margin);left:var(--block-label-margin)}label.svelte-13ao5pu.svelte-13ao5pu:not(.float){position:static;margin-top:var(--block-label-margin);margin-left:var(--block-label-margin)}.hide.svelte-13ao5pu.svelte-13ao5pu{height:0}span.svelte-13ao5pu.svelte-13ao5pu{opacity:.8;margin-right:var(--size-2);width:calc(var(--block-label-text-size) - 1px);height:calc(var(--block-label-text-size) - 1px)}.hide-label.svelte-13ao5pu.svelte-13ao5pu{box-shadow:none;border-width:0;background:transparent;overflow:visible}label[dir=rtl].svelte-13ao5pu.svelte-13ao5pu{border:var(--block-label-border-width) solid var(--block-label-border-color);border-top:none;border-right:none;border-bottom-left-radius:var(--block-radius);border-bottom-right-radius:var(--block-label-radius);border-top-left-radius:var(--block-label-radius)}label[dir=rtl].svelte-13ao5pu span.svelte-13ao5pu{margin-left:var(--size-2);margin-right:0}button.svelte-qgco6m{display:flex;justify-content:center;align-items:center;gap:1px;z-index:var(--layer-2);border-radius:var(--radius-xs);color:var(--block-label-text-color);border:1px solid transparent;padding:var(--spacing-xxs)}button.svelte-qgco6m:hover{background-color:var(--background-fill-secondary)}button[disabled].svelte-qgco6m{opacity:.5;box-shadow:none}button[disabled].svelte-qgco6m:hover{cursor:not-allowed}.padded.svelte-qgco6m{background:var(--bg-color)}button.svelte-qgco6m:hover,button.highlight.svelte-qgco6m{cursor:pointer;color:var(--color-accent)}.padded.svelte-qgco6m:hover{color:var(--block-label-text-color)}span.svelte-qgco6m{padding:0 1px;font-size:10px}div.svelte-qgco6m{display:flex;align-items:center;justify-content:center;transition:filter .2s ease-in-out}.x-small.svelte-qgco6m{width:10px;height:10px}.small.svelte-qgco6m{width:14px;height:14px}.medium.svelte-qgco6m{width:20px;height:20px}.large.svelte-qgco6m{width:22px;height:22px}.pending.svelte-qgco6m{animation:svelte-qgco6m-flash .5s infinite}@keyframes svelte-qgco6m-flash{0%{opacity:.5}50%{opacity:1}to{opacity:.5}}.transparent.svelte-qgco6m{background:transparent;border:none;box-shadow:none}.empty.svelte-3w3rth{display:flex;justify-content:center;align-items:center;margin-top:calc(0px - var(--size-6));height:var(--size-full)}.icon.svelte-3w3rth{opacity:.5;height:var(--size-5);color:var(--body-text-color)}.small.svelte-3w3rth{min-height:calc(var(--size-32) - 20px)}.large.svelte-3w3rth{min-height:calc(var(--size-64) - 20px)}.unpadded_box.svelte-3w3rth{margin-top:0}.small_parent.svelte-3w3rth{min-height:100%!important}.dropdown-arrow.svelte-145leq6,.dropdown-arrow.svelte-ihhdbf{fill:currentColor}.circle.svelte-ihhdbf{fill:currentColor;opacity:.1}svg.svelte-pb9pol{animation:svelte-pb9pol-spin 1.5s linear infinite}@keyframes svelte-pb9pol-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}h2.svelte-1xg7h5n{font-size:var(--text-xl)!important}p.svelte-1xg7h5n,h2.svelte-1xg7h5n{white-space:pre-line}.wrap.svelte-1xg7h5n{display:flex;flex-direction:column;justify-content:center;align-items:center;min-height:var(--size-60);color:var(--block-label-text-color);line-height:var(--line-md);height:100%;padding-top:var(--size-3);text-align:center;margin:auto var(--spacing-lg)}.or.svelte-1xg7h5n{color:var(--body-text-color-subdued);display:flex}.icon-wrap.svelte-1xg7h5n{width:30px;margin-bottom:var(--spacing-lg)}@media (--screen-md){.wrap.svelte-1xg7h5n{font-size:var(--text-lg)}}.hovered.svelte-1xg7h5n{color:var(--color-accent)}div.svelte-q32hvf{border-top:1px solid transparent;display:flex;max-height:100%;justify-content:center;align-items:center;gap:var(--spacing-sm);height:auto;align-items:flex-end;color:var(--block-label-text-color);flex-shrink:0}.show_border.svelte-q32hvf{border-top:1px solid var(--block-border-color);margin-top:var(--spacing-xxl);box-shadow:var(--shadow-drop)}.source-selection.svelte-15ls1gu{display:flex;align-items:center;justify-content:center;border-top:1px solid var(--border-color-primary);width:100%;margin-left:auto;margin-right:auto;height:var(--size-10)}.icon.svelte-15ls1gu{width:22px;height:22px;margin:var(--spacing-lg) var(--spacing-xs);padding:var(--spacing-xs);color:var(--neutral-400);border-radius:var(--radius-md)}.selected.svelte-15ls1gu{color:var(--color-accent)}.icon.svelte-15ls1gu:hover,.icon.svelte-15ls1gu:focus{color:var(--color-accent)}.icon-button-wrapper.svelte-109se4{display:flex;flex-direction:row;align-items:center;justify-content:center;z-index:var(--layer-3);gap:var(--spacing-sm);box-shadow:var(--shadow-drop);border:1px solid var(--border-color-primary);background:var(--block-background-fill);padding:var(--spacing-xxs)}.icon-button-wrapper.hide-top-corner.svelte-109se4{border-top:none;border-right:none;border-radius:var(--block-label-right-radius)}.icon-button-wrapper.display-top-corner.svelte-109se4{border-radius:var(--radius-sm) 0 0 var(--radius-sm);top:var(--spacing-sm);right:-1px}.icon-button-wrapper.svelte-109se4:not(.top-panel){border:1px solid var(--border-color-primary);border-radius:var(--radius-sm)}.top-panel.svelte-109se4{position:absolute;top:var(--block-label-margin);right:var(--block-label-margin);margin:0}.icon-button-wrapper.svelte-109se4 button{margin:var(--spacing-xxs);border-radius:var(--radius-xs);position:relative}.icon-button-wrapper.svelte-109se4 a.download-link:not(:last-child),.icon-button-wrapper.svelte-109se4 button:not(:last-child){margin-right:var(--spacing-xxs)}.icon-button-wrapper.svelte-109se4 a.download-link:not(:last-child):not(.no-border *):after,.icon-button-wrapper.svelte-109se4 button:not(:last-child):not(.no-border *):after{content:"";position:absolute;right:-4.5px;top:15%;height:70%;width:1px;background-color:var(--border-color-primary)}.icon-button-wrapper.svelte-109se4>*{height:100%}.unstyled-link.svelte-151nsdd{all:unset;cursor:pointer}img.svelte-kxeri3{object-fit:cover}.image-container.svelte-ui4ajv.svelte-ui4ajv{height:100%;position:relative;min-width:var(--size-20)}.image-container.svelte-ui4ajv button.svelte-ui4ajv{width:var(--size-full);height:var(--size-full);border-radius:var(--radius-lg);display:flex;align-items:center;justify-content:center}.image-frame.svelte-ui4ajv img{width:var(--size-full);height:var(--size-full);object-fit:scale-down}.selectable.svelte-ui4ajv.svelte-ui4ajv{cursor:crosshair}.fullscreen-controls svg{position:relative;top:0}.image-container:fullscreen{background-color:#000;display:flex;justify-content:center;align-items:center}.image-container:fullscreen img{max-width:90vw;max-height:90vh;object-fit:scale-down}.image-frame.svelte-ui4ajv.svelte-ui4ajv{width:auto;height:100%;display:flex;align-items:center;justify-content:center}.metadata-popup.svelte-ui4ajv.svelte-ui4ajv{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--background-fill-primary, white);border:1px solid var(--border-color-primary);box-shadow:0 4px 8px #0003;z-index:1000;border-radius:8px;overflow:hidden}.popup-content.svelte-ui4ajv.svelte-ui4ajv{position:relative;padding:1rem;display:flex;flex-direction:column;height:100%}.popup-title.svelte-ui4ajv.svelte-ui4ajv{font-weight:700;margin:0 0 1rem}.close-button.svelte-ui4ajv.svelte-ui4ajv{position:absolute;top:.5rem;right:.5rem;background:none;border:none;font-size:1rem;cursor:pointer}.metadata-table-container.svelte-ui4ajv.svelte-ui4ajv{flex:1;overflow:auto}.metadata-table.svelte-ui4ajv.svelte-ui4ajv{width:100%;border-collapse:collapse}.metadata-label.svelte-ui4ajv.svelte-ui4ajv{background:var(--background-fill-secondary, #f5f5f5);padding:.5rem;font-weight:700;text-align:left;vertical-align:top;width:40%}.metadata-value.svelte-ui4ajv.svelte-ui4ajv{padding:.5rem;white-space:nowrap;vertical-align:top}.load-metadata-button.svelte-ui4ajv.svelte-ui4ajv{margin-top:1rem;padding:.5rem 1rem;background-color:var(--button-primary-border-color);color:var(--button-primary-text-color);border:none;border-radius:4px;cursor:pointer;align-self:center}.load-metadata-button.svelte-ui4ajv.svelte-ui4ajv:hover{background-color:var(--button-primary-background-fill-hover)}.wrap.svelte-cr2edf.svelte-cr2edf{overflow-y:auto;transition:opacity .5s ease-in-out;background:var(--block-background-fill);position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:var(--size-40);width:var(--size-full)}.wrap.svelte-cr2edf.svelte-cr2edf:after{content:"";position:absolute;top:0;left:0;width:var(--upload-progress-width);height:100%;transition:all .5s ease-in-out;z-index:1}.uploading.svelte-cr2edf.svelte-cr2edf{font-size:var(--text-lg);font-family:var(--font);z-index:2}.file-name.svelte-cr2edf.svelte-cr2edf{margin:var(--spacing-md);font-size:var(--text-lg);color:var(--body-text-color-subdued)}.file.svelte-cr2edf.svelte-cr2edf{font-size:var(--text-md);z-index:2;display:flex;align-items:center}.file.svelte-cr2edf progress.svelte-cr2edf{display:inline;height:var(--size-1);width:100%;transition:all .5s ease-in-out;color:var(--color-accent);border:none}.file.svelte-cr2edf progress[value].svelte-cr2edf::-webkit-progress-value{background-color:var(--color-accent);border-radius:20px}.file.svelte-cr2edf progress[value].svelte-cr2edf::-webkit-progress-bar{background-color:var(--border-color-accent);border-radius:20px}.progress-bar.svelte-cr2edf.svelte-cr2edf{width:14px;height:14px;border-radius:50%;background:radial-gradient(closest-side,var(--block-background-fill) 64%,transparent 53% 100%),conic-gradient(var(--color-accent) var(--upload-progress-width),var(--border-color-accent) 0);transition:all .5s ease-in-out}button.svelte-1o7nwih{cursor:pointer;width:var(--size-full)}.center.svelte-1o7nwih{display:flex;justify-content:center}.flex.svelte-1o7nwih{display:flex;flex-direction:column;justify-content:center;align-items:center}.hidden.svelte-1o7nwih{display:none;position:absolute;flex-grow:0}.hidden.svelte-1o7nwih svg{display:none}.disable_click.svelte-1o7nwih{cursor:default}.icon-mode.svelte-1o7nwih{position:absolute!important;width:var(--size-4);height:var(--size-4);padding:0;min-height:0;border-radius:var(--radius-circle)}.icon-mode.svelte-1o7nwih svg{width:var(--size-4);height:var(--size-4)}.image-frame.svelte-18daxnr img{width:var(--size-full);height:var(--size-full);object-fit:scale-down}.upload-container.svelte-18daxnr{display:flex;align-items:center;justify-content:center;height:100%;flex-shrink:1;max-height:100%}.image-container.svelte-18daxnr{display:flex;height:100%;flex-direction:column;justify-content:center;align-items:center;max-height:100%;position:relative}.selectable.svelte-18daxnr{cursor:crosshair}.image-frame.svelte-18daxnr{object-fit:cover;width:100%;height:100%}.metadata-popup.svelte-18daxnr{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--background-fill-primary, white);border:1px solid var(--border-color-primary);box-shadow:0 4px 8px #0003;z-index:1000;border-radius:8px;overflow:hidden}.popup-content.svelte-18daxnr{position:relative;padding:1rem;display:flex;flex-direction:column;height:100%}.popup-title.svelte-18daxnr{font-weight:700;margin:0 0 1rem}.close-button.svelte-18daxnr{position:absolute;top:.5rem;right:.5rem;background:none;border:none;font-size:1rem;cursor:pointer}.metadata-table-container.svelte-18daxnr{flex:1;overflow:auto}.metadata-table.svelte-18daxnr{width:100%;border-collapse:collapse}.metadata-label.svelte-18daxnr{background:var(--background-fill-secondary, #f5f5f5);padding:.5rem;font-weight:700;text-align:left;vertical-align:top;width:40%}.metadata-value.svelte-18daxnr{padding:.5rem;white-space:nowrap;vertical-align:top}.load-metadata-button.svelte-18daxnr{margin-top:1rem;padding:.5rem 1rem;background-color:var(--button-primary-border-color);color:var(--button-primary-text-color);border:none;border-radius:4px;cursor:pointer;align-self:center}.load-metadata-button.svelte-18daxnr:hover{background-color:var(--button-primary-background-fill-hover)}svg.svelte-43sxxs.svelte-43sxxs{width:var(--size-20);height:var(--size-20)}svg.svelte-43sxxs path.svelte-43sxxs{fill:var(--loader-color)}div.svelte-43sxxs.svelte-43sxxs{z-index:var(--layer-2)}.margin.svelte-43sxxs.svelte-43sxxs{margin:var(--size-4)}.wrap.svelte-17v219f.svelte-17v219f{display:flex;flex-direction:column;justify-content:center;align-items:center;z-index:var(--layer-2);transition:opacity .1s ease-in-out;border-radius:var(--block-radius);background:var(--block-background-fill);padding:0 var(--size-6);max-height:var(--size-screen-h);overflow:hidden}.wrap.center.svelte-17v219f.svelte-17v219f{top:0;right:0;left:0}.wrap.default.svelte-17v219f.svelte-17v219f{top:0;right:0;bottom:0;left:0}.hide.svelte-17v219f.svelte-17v219f{opacity:0;pointer-events:none}.generating.svelte-17v219f.svelte-17v219f{animation:svelte-17v219f-pulseStart 1s cubic-bezier(.4,0,.6,1),svelte-17v219f-pulse 2s cubic-bezier(.4,0,.6,1) 1s infinite;border:2px solid var(--color-accent);background:transparent;z-index:var(--layer-1);pointer-events:none}.translucent.svelte-17v219f.svelte-17v219f{background:none}@keyframes svelte-17v219f-pulseStart{0%{opacity:0}to{opacity:1}}@keyframes svelte-17v219f-pulse{0%,to{opacity:1}50%{opacity:.5}}.loading.svelte-17v219f.svelte-17v219f{z-index:var(--layer-2);color:var(--body-text-color)}.eta-bar.svelte-17v219f.svelte-17v219f{position:absolute;top:0;right:0;bottom:0;left:0;transform-origin:left;opacity:.8;z-index:var(--layer-1);transition:10ms;background:var(--background-fill-secondary)}.progress-bar-wrap.svelte-17v219f.svelte-17v219f{border:1px solid var(--border-color-primary);background:var(--background-fill-primary);width:55.5%;height:var(--size-4)}.progress-bar.svelte-17v219f.svelte-17v219f{transform-origin:left;background-color:var(--loader-color);width:var(--size-full);height:var(--size-full)}.progress-level.svelte-17v219f.svelte-17v219f{display:flex;flex-direction:column;align-items:center;gap:1;z-index:var(--layer-2);width:var(--size-full)}.progress-level-inner.svelte-17v219f.svelte-17v219f{margin:var(--size-2) auto;color:var(--body-text-color);font-size:var(--text-sm);font-family:var(--font-mono)}.meta-text.svelte-17v219f.svelte-17v219f{position:absolute;bottom:0;right:0;z-index:var(--layer-2);padding:var(--size-1) var(--size-2);font-size:var(--text-sm);font-family:var(--font-mono)}.meta-text-center.svelte-17v219f.svelte-17v219f{display:flex;position:absolute;top:0;right:0;justify-content:center;align-items:center;transform:translateY(var(--size-6));z-index:var(--layer-2);padding:var(--size-1) var(--size-2);font-size:var(--text-sm);font-family:var(--font-mono);text-align:center}.error.svelte-17v219f.svelte-17v219f{box-shadow:var(--shadow-drop);border:solid 1px var(--error-border-color);border-radius:var(--radius-full);background:var(--error-background-fill);padding-right:var(--size-4);padding-left:var(--size-4);color:var(--error-text-color);font-weight:var(--weight-semibold);font-size:var(--text-lg);line-height:var(--line-lg);font-family:var(--font)}.minimal.svelte-17v219f.svelte-17v219f{pointer-events:none}.minimal.svelte-17v219f .progress-text.svelte-17v219f{background:var(--block-background-fill)}.border.svelte-17v219f.svelte-17v219f{border:1px solid var(--border-color-primary)}.clear-status.svelte-17v219f.svelte-17v219f{position:absolute;display:flex;top:var(--size-2);right:var(--size-2);justify-content:flex-end;gap:var(--spacing-sm);z-index:var(--layer-1)}.toast-body.svelte-syezpc{display:flex;position:relative;right:0;left:0;align-items:center;margin:var(--size-6) var(--size-4);margin:auto;border-radius:var(--container-radius);overflow:hidden;pointer-events:auto}.toast-body.error.svelte-syezpc{border:1px solid var(--color-red-700);background:var(--color-red-50)}.dark .toast-body.error.svelte-syezpc{border:1px solid var(--color-red-500);background-color:var(--color-grey-950)}.toast-body.warning.svelte-syezpc{border:1px solid var(--color-yellow-700);background:var(--color-yellow-50)}.dark .toast-body.warning.svelte-syezpc{border:1px solid var(--color-yellow-500);background-color:var(--color-grey-950)}.toast-body.info.svelte-syezpc{border:1px solid var(--color-grey-700);background:var(--color-grey-50)}.dark .toast-body.info.svelte-syezpc{border:1px solid var(--color-grey-500);background-color:var(--color-grey-950)}.toast-body.success.svelte-syezpc{border:1px solid var(--color-green-700);background:var(--color-green-50)}.dark .toast-body.success.svelte-syezpc{border:1px solid var(--color-green-500);background-color:var(--color-grey-950)}.toast-title.svelte-syezpc{display:flex;align-items:center;font-weight:var(--weight-bold);font-size:var(--text-lg);line-height:var(--line-sm)}.toast-title.error.svelte-syezpc{color:var(--color-red-700)}.dark .toast-title.error.svelte-syezpc{color:var(--color-red-50)}.toast-title.warning.svelte-syezpc{color:var(--color-yellow-700)}.dark .toast-title.warning.svelte-syezpc{color:var(--color-yellow-50)}.toast-title.info.svelte-syezpc{color:var(--color-grey-700)}.dark .toast-title.info.svelte-syezpc{color:var(--color-grey-50)}.toast-title.success.svelte-syezpc{color:var(--color-green-700)}.dark .toast-title.success.svelte-syezpc{color:var(--color-green-50)}.toast-close.svelte-syezpc{margin:0 var(--size-3);border-radius:var(--size-3);padding:0px var(--size-1-5);font-size:var(--size-5);line-height:var(--size-5)}.toast-close.error.svelte-syezpc{color:var(--color-red-700)}.dark .toast-close.error.svelte-syezpc{color:var(--color-red-500)}.toast-close.warning.svelte-syezpc{color:var(--color-yellow-700)}.dark .toast-close.warning.svelte-syezpc{color:var(--color-yellow-500)}.toast-close.info.svelte-syezpc{color:var(--color-grey-700)}.dark .toast-close.info.svelte-syezpc{color:var(--color-grey-500)}.toast-close.success.svelte-syezpc{color:var(--color-green-700)}.dark .toast-close.success.svelte-syezpc{color:var(--color-green-500)}.toast-text.svelte-syezpc{font-size:var(--text-lg);word-wrap:break-word;overflow-wrap:break-word;word-break:break-word}.toast-text.error.svelte-syezpc{color:var(--color-red-700)}.dark .toast-text.error.svelte-syezpc{color:var(--color-red-50)}.toast-text.warning.svelte-syezpc{color:var(--color-yellow-700)}.dark .toast-text.warning.svelte-syezpc{color:var(--color-yellow-50)}.toast-text.info.svelte-syezpc{color:var(--color-grey-700)}.dark .toast-text.info.svelte-syezpc{color:var(--color-grey-50)}.toast-text.success.svelte-syezpc{color:var(--color-green-700)}.dark .toast-text.success.svelte-syezpc{color:var(--color-green-50)}.toast-details.svelte-syezpc{margin:var(--size-3) var(--size-3) var(--size-3) 0;width:100%}.toast-icon.svelte-syezpc{display:flex;position:absolute;position:relative;flex-shrink:0;justify-content:center;align-items:center;margin:var(--size-2);border-radius:var(--radius-full);padding:var(--size-1);padding-left:calc(var(--size-1) - 1px);width:35px;height:35px}.toast-icon.error.svelte-syezpc{color:var(--color-red-700)}.dark .toast-icon.error.svelte-syezpc{color:var(--color-red-500)}.toast-icon.warning.svelte-syezpc{color:var(--color-yellow-700)}.dark .toast-icon.warning.svelte-syezpc{color:var(--color-yellow-500)}.toast-icon.info.svelte-syezpc{color:var(--color-grey-700)}.dark .toast-icon.info.svelte-syezpc{color:var(--color-grey-500)}.toast-icon.success.svelte-syezpc{color:var(--color-green-700)}.dark .toast-icon.success.svelte-syezpc{color:var(--color-green-500)}@keyframes svelte-syezpc-countdown{0%{transform:scaleX(1)}to{transform:scaleX(0)}}.timer.svelte-syezpc{position:absolute;bottom:0;left:0;transform-origin:0 0;animation:svelte-syezpc-countdown 10s linear forwards;width:100%;height:var(--size-1)}.timer.error.svelte-syezpc{background:var(--color-red-700)}.dark .timer.error.svelte-syezpc{background:var(--color-red-500)}.timer.warning.svelte-syezpc{background:var(--color-yellow-700)}.dark .timer.warning.svelte-syezpc{background:var(--color-yellow-500)}.timer.info.svelte-syezpc{background:var(--color-grey-700)}.dark .timer.info.svelte-syezpc{background:var(--color-grey-500)}.timer.success.svelte-syezpc{background:var(--color-green-700)}.dark .timer.success.svelte-syezpc{background:var(--color-green-500)}.hidden.svelte-syezpc{display:none}.toast-text.svelte-syezpc a{text-decoration:underline}.toast-wrap.svelte-gatr8h{display:flex;position:fixed;top:var(--size-4);right:var(--size-4);flex-direction:column;align-items:end;gap:var(--size-2);z-index:var(--layer-top);width:calc(100% - var(--size-8))}@media (--screen-sm){.toast-wrap.svelte-gatr8h{width:calc(var(--size-96) + var(--size-10))}}.streaming-bar.svelte-ga0jj6{position:absolute;bottom:0;left:0;right:0;height:4px;background-color:var(--primary-600);animation:svelte-ga0jj6-countdown linear forwards;z-index:1}@keyframes svelte-ga0jj6-countdown{0%{transform:translate(0)}to{transform:translate(-100%)}}.container.svelte-1sgcyba img{width:100%;height:100%}.container.selected.svelte-1sgcyba{border-color:var(--border-color-accent)}.border.table.svelte-1sgcyba{border:2px solid var(--border-color-primary)}.container.table.svelte-1sgcyba{margin:0 auto;border-radius:var(--radius-lg);overflow:hidden;width:var(--size-20);height:var(--size-20);object-fit:cover}.container.gallery.svelte-1sgcyba{width:var(--size-20);max-width:var(--size-20);object-fit:cover}
         | 
    	
        src/demo/app.py
    CHANGED
    
    | @@ -7,6 +7,7 @@ from gradio_propertysheet import PropertySheet | |
| 7 | 
             
            from gradio_propertysheet.helpers import build_dataclass_fields, create_dataclass_instance
         | 
| 8 | 
             
            from pathlib import Path
         | 
| 9 |  | 
|  | |
| 10 | 
             
            output_dir = Path("outputs")
         | 
| 11 | 
             
            output_dir.mkdir(exist_ok=True)
         | 
| 12 |  | 
| @@ -27,6 +28,51 @@ class PropertyConfig: | |
| 27 | 
             
                image_settings: ImageSettings = field(default_factory=ImageSettings)
         | 
| 28 | 
             
                description: str = field(default="", metadata={"label": "Description"})
         | 
| 29 |  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 30 | 
             
            def handle_load_metadata(image_data: ImageMeta | None) -> List[Any]:
         | 
| 31 | 
             
                """
         | 
| 32 | 
             
                Processes image metadata and maps it to output components.
         | 
| @@ -45,11 +91,11 @@ def handle_load_metadata(image_data: ImageMeta | None) -> List[Any]: | |
| 45 | 
             
                raw_values = transfer_metadata(output_fields, metadata, dataclass_fields)
         | 
| 46 |  | 
| 47 | 
             
                output_values = [gr.skip()] * len(output_fields)
         | 
| 48 | 
            -
                for i, (component, value) in enumerate(zip(output_fields, raw_values)):
         | 
| 49 | 
             
                    if hasattr(component, 'root_label'):
         | 
| 50 | 
             
                        output_values[i] = create_dataclass_instance(PropertyConfig, value)
         | 
| 51 | 
             
                    else:
         | 
| 52 | 
            -
                        output_values[i] = gr. | 
| 53 |  | 
| 54 | 
             
                return output_values
         | 
| 55 |  | 
| @@ -68,11 +114,11 @@ def save_image_with_metadata(image_data: Any, *inputs: Any) -> str | None: | |
| 68 | 
             
                    return None
         | 
| 69 |  | 
| 70 | 
             
                params = list(inputs)
         | 
| 71 | 
            -
                image_params = dict(zip(input_fields.keys(), params))
         | 
| 72 | 
            -
                 | 
| 73 | 
            -
                metadata = {label: image_params.get(label, "") for label in dataclass_fields.keys()}
         | 
| 74 |  | 
| 75 | 
             
                new_filepath = output_dir / "image_with_meta.png"
         | 
|  | |
| 76 | 
             
                add_metadata(image_data, metadata, new_filepath)
         | 
| 77 |  | 
| 78 | 
             
                return str(new_filepath)
         | 
|  | |
| 7 | 
             
            from gradio_propertysheet.helpers import build_dataclass_fields, create_dataclass_instance
         | 
| 8 | 
             
            from pathlib import Path
         | 
| 9 |  | 
| 10 | 
            +
             | 
| 11 | 
             
            output_dir = Path("outputs")
         | 
| 12 | 
             
            output_dir.mkdir(exist_ok=True)
         | 
| 13 |  | 
|  | |
| 28 | 
             
                image_settings: ImageSettings = field(default_factory=ImageSettings)
         | 
| 29 | 
             
                description: str = field(default="", metadata={"label": "Description"})
         | 
| 30 |  | 
| 31 | 
            +
            def infer_type(s: str):
         | 
| 32 | 
            +
                """
         | 
| 33 | 
            +
                Infers and converts a string to the most likely data type.
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                It attempts conversions in the following order:
         | 
| 36 | 
            +
                1. Integer
         | 
| 37 | 
            +
                2. Float
         | 
| 38 | 
            +
                3. Boolean (case-insensitive 'true' or 'false')
         | 
| 39 | 
            +
                If all conversions fail, it returns the original string.
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                Args:
         | 
| 42 | 
            +
                    s: The input string to be converted.
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                Returns:
         | 
| 45 | 
            +
                    The converted value (int, float, bool) or the original string.
         | 
| 46 | 
            +
                """
         | 
| 47 | 
            +
                if not isinstance(s, str):
         | 
| 48 | 
            +
                    # If the input is not a string, return it as is.
         | 
| 49 | 
            +
                    return s
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                # 1. Try to convert to an integer
         | 
| 52 | 
            +
                try:
         | 
| 53 | 
            +
                    return int(s)
         | 
| 54 | 
            +
                except ValueError:
         | 
| 55 | 
            +
                    # Not an integer, continue...
         | 
| 56 | 
            +
                    pass
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                # 2. Try to convert to a float
         | 
| 59 | 
            +
                try:
         | 
| 60 | 
            +
                    return float(s)
         | 
| 61 | 
            +
                except ValueError:
         | 
| 62 | 
            +
                    # Not a float, continue...
         | 
| 63 | 
            +
                    pass
         | 
| 64 | 
            +
                
         | 
| 65 | 
            +
                # 3. Check for a boolean value
         | 
| 66 | 
            +
                # This explicit check is important because bool('False') evaluates to True.
         | 
| 67 | 
            +
                s_lower = s.lower()
         | 
| 68 | 
            +
                if s_lower == 'true':
         | 
| 69 | 
            +
                    return True
         | 
| 70 | 
            +
                if s_lower == 'false':
         | 
| 71 | 
            +
                    return False
         | 
| 72 | 
            +
                    
         | 
| 73 | 
            +
                # 4. If nothing else worked, return the original string
         | 
| 74 | 
            +
                return s
         | 
| 75 | 
            +
             | 
| 76 | 
             
            def handle_load_metadata(image_data: ImageMeta | None) -> List[Any]:
         | 
| 77 | 
             
                """
         | 
| 78 | 
             
                Processes image metadata and maps it to output components.
         | 
|  | |
| 91 | 
             
                raw_values = transfer_metadata(output_fields, metadata, dataclass_fields)
         | 
| 92 |  | 
| 93 | 
             
                output_values = [gr.skip()] * len(output_fields)
         | 
| 94 | 
            +
                for i, (component, value) in enumerate(zip(output_fields, raw_values)):        
         | 
| 95 | 
             
                    if hasattr(component, 'root_label'):
         | 
| 96 | 
             
                        output_values[i] = create_dataclass_instance(PropertyConfig, value)
         | 
| 97 | 
             
                    else:
         | 
| 98 | 
            +
                        output_values[i] = gr.update(value=infer_type(value))
         | 
| 99 |  | 
| 100 | 
             
                return output_values
         | 
| 101 |  | 
|  | |
| 114 | 
             
                    return None
         | 
| 115 |  | 
| 116 | 
             
                params = list(inputs)
         | 
| 117 | 
            +
                image_params = dict(zip(input_fields.keys(), params))    
         | 
| 118 | 
            +
                metadata = {label: image_params.get(label, "") for label in image_params.keys()}
         | 
|  | |
| 119 |  | 
| 120 | 
             
                new_filepath = output_dir / "image_with_meta.png"
         | 
| 121 | 
            +
                
         | 
| 122 | 
             
                add_metadata(image_data, metadata, new_filepath)
         | 
| 123 |  | 
| 124 | 
             
                return str(new_filepath)
         | 
    	
        src/demo/space.py
    CHANGED
    
    | @@ -21,7 +21,7 @@ with gr.Blocks( | |
| 21 | 
             
            # `gradio_imagemeta`
         | 
| 22 |  | 
| 23 | 
             
            <div style="display: flex; gap: 7px;">
         | 
| 24 | 
            -
            <img alt=" | 
| 25 | 
             
            </div>
         | 
| 26 |  | 
| 27 | 
             
            Image Preview with Metadata for Gradio Interface
         | 
| @@ -47,6 +47,7 @@ from gradio_propertysheet import PropertySheet | |
| 47 | 
             
            from gradio_propertysheet.helpers import build_dataclass_fields, create_dataclass_instance
         | 
| 48 | 
             
            from pathlib import Path
         | 
| 49 |  | 
|  | |
| 50 | 
             
            output_dir = Path("outputs")
         | 
| 51 | 
             
            output_dir.mkdir(exist_ok=True)
         | 
| 52 |  | 
| @@ -67,25 +68,50 @@ class PropertyConfig: | |
| 67 | 
             
                image_settings: ImageSettings = field(default_factory=ImageSettings)
         | 
| 68 | 
             
                description: str = field(default="", metadata={"label": "Description"})
         | 
| 69 |  | 
| 70 | 
            -
            def  | 
| 71 | 
             
                \"\"\"
         | 
| 72 | 
            -
                 | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 73 |  | 
| 74 | 
             
                Args:
         | 
| 75 | 
            -
                     | 
| 76 | 
            -
                    img_all_path: File path for the image to display in img_all.
         | 
| 77 |  | 
| 78 | 
             
                Returns:
         | 
| 79 | 
            -
                     | 
| 80 | 
             
                \"\"\"
         | 
| 81 | 
            -
                 | 
| 82 | 
            -
             | 
| 83 | 
            -
                     | 
| 84 | 
            -
             | 
| 85 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 86 |  | 
| 87 | 
            -
                #  | 
| 88 | 
            -
                 | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 89 |  | 
| 90 | 
             
            def handle_load_metadata(image_data: ImageMeta | None) -> List[Any]:
         | 
| 91 | 
             
                \"\"\"
         | 
| @@ -105,11 +131,11 @@ def handle_load_metadata(image_data: ImageMeta | None) -> List[Any]: | |
| 105 | 
             
                raw_values = transfer_metadata(output_fields, metadata, dataclass_fields)
         | 
| 106 |  | 
| 107 | 
             
                output_values = [gr.skip()] * len(output_fields)
         | 
| 108 | 
            -
                for i, (component, value) in enumerate(zip(output_fields, raw_values)):
         | 
| 109 | 
             
                    if hasattr(component, 'root_label'):
         | 
| 110 | 
             
                        output_values[i] = create_dataclass_instance(PropertyConfig, value)
         | 
| 111 | 
             
                    else:
         | 
| 112 | 
            -
                        output_values[i] = gr. | 
| 113 |  | 
| 114 | 
             
                return output_values
         | 
| 115 |  | 
| @@ -129,10 +155,11 @@ def save_image_with_metadata(image_data: Any, *inputs: Any) -> str | None: | |
| 129 |  | 
| 130 | 
             
                params = list(inputs)
         | 
| 131 | 
             
                image_params = dict(zip(input_fields.keys(), params))
         | 
| 132 | 
            -
                dataclass_fields = build_dataclass_fields(PropertyConfig)
         | 
| 133 | 
            -
                metadata = {label: image_params.get(label, "") for label in  | 
| 134 |  | 
| 135 | 
             
                new_filepath = output_dir / "image_with_meta.png"
         | 
|  | |
| 136 | 
             
                add_metadata(image_data, metadata, new_filepath)
         | 
| 137 |  | 
| 138 | 
             
                return str(new_filepath)
         | 
| @@ -157,17 +184,18 @@ with gr.Blocks() as demo: | |
| 157 | 
             
                        label="Upload Image (Custom metadata only)",
         | 
| 158 | 
             
                        type="filepath",
         | 
| 159 | 
             
                        width=300,
         | 
| 160 | 
            -
                        height=400,
         | 
| 161 | 
            -
                        disable_preprocess=False,
         | 
| 162 | 
             
                        interactive=True
         | 
| 163 | 
             
                    )
         | 
| 164 | 
             
                    img_all = ImageMeta(
         | 
| 165 | 
             
                        label="Upload Image (All metadata)",
         | 
| 166 | 
             
                        only_custom_metadata=False,
         | 
|  | |
| 167 | 
             
                        width=300,
         | 
| 168 | 
            -
                        height=400,
         | 
| 169 | 
             
                        popup_metadata_height=400,
         | 
| 170 | 
            -
                        popup_metadata_width=500
         | 
|  | |
| 171 | 
             
                    )
         | 
| 172 |  | 
| 173 | 
             
                gr.Markdown("## Metadata Viewer")
         | 
| @@ -193,17 +221,7 @@ with gr.Blocks() as demo: | |
| 193 | 
             
                with gr.Row():
         | 
| 194 | 
             
                    save_button = gr.Button("Add Metadata and Save Image")
         | 
| 195 | 
             
                    saved_file_output = gr.File(label="Download Image")
         | 
| 196 | 
            -
             | 
| 197 | 
            -
                with gr.Row():
         | 
| 198 | 
            -
                    gr.Examples(
         | 
| 199 | 
            -
                        examples=[
         | 
| 200 | 
            -
                            ["./examples/image_with_meta.png", "./examples/image_with_meta.png"]
         | 
| 201 | 
            -
                        ],
         | 
| 202 | 
            -
                        fn=process_example_images,
         | 
| 203 | 
            -
                        inputs=[img_custom, img_all],
         | 
| 204 | 
            -
                        outputs=[img_custom, img_all],
         | 
| 205 | 
            -
                        cache_examples=True
         | 
| 206 | 
            -
                    )
         | 
| 207 |  | 
| 208 | 
             
                input_fields = {
         | 
| 209 | 
             
                    "Model": model_box,
         | 
|  | |
| 21 | 
             
            # `gradio_imagemeta`
         | 
| 22 |  | 
| 23 | 
             
            <div style="display: flex; gap: 7px;">
         | 
| 24 | 
            +
            <a href="https://pypi.org/project/gradio_imagemeta/" target="_blank"><img alt="PyPI - Version" src="https://img.shields.io/pypi/v/gradio_imagemeta"></a>  
         | 
| 25 | 
             
            </div>
         | 
| 26 |  | 
| 27 | 
             
            Image Preview with Metadata for Gradio Interface
         | 
|  | |
| 47 | 
             
            from gradio_propertysheet.helpers import build_dataclass_fields, create_dataclass_instance
         | 
| 48 | 
             
            from pathlib import Path
         | 
| 49 |  | 
| 50 | 
            +
             | 
| 51 | 
             
            output_dir = Path("outputs")
         | 
| 52 | 
             
            output_dir.mkdir(exist_ok=True)
         | 
| 53 |  | 
|  | |
| 68 | 
             
                image_settings: ImageSettings = field(default_factory=ImageSettings)
         | 
| 69 | 
             
                description: str = field(default="", metadata={"label": "Description"})
         | 
| 70 |  | 
| 71 | 
            +
            def infer_type(s: str):
         | 
| 72 | 
             
                \"\"\"
         | 
| 73 | 
            +
                Infers and converts a string to the most likely data type.
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                It attempts conversions in the following order:
         | 
| 76 | 
            +
                1. Integer
         | 
| 77 | 
            +
                2. Float
         | 
| 78 | 
            +
                3. Boolean (case-insensitive 'true' or 'false')
         | 
| 79 | 
            +
                If all conversions fail, it returns the original string.
         | 
| 80 |  | 
| 81 | 
             
                Args:
         | 
| 82 | 
            +
                    s: The input string to be converted.
         | 
|  | |
| 83 |  | 
| 84 | 
             
                Returns:
         | 
| 85 | 
            +
                    The converted value (int, float, bool) or the original string.
         | 
| 86 | 
             
                \"\"\"
         | 
| 87 | 
            +
                if not isinstance(s, str):
         | 
| 88 | 
            +
                    # If the input is not a string, return it as is.
         | 
| 89 | 
            +
                    return s
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                # 1. Try to convert to an integer
         | 
| 92 | 
            +
                try:
         | 
| 93 | 
            +
                    return int(s)
         | 
| 94 | 
            +
                except ValueError:
         | 
| 95 | 
            +
                    # Not an integer, continue...
         | 
| 96 | 
            +
                    pass
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                # 2. Try to convert to a float
         | 
| 99 | 
            +
                try:
         | 
| 100 | 
            +
                    return float(s)
         | 
| 101 | 
            +
                except ValueError:
         | 
| 102 | 
            +
                    # Not a float, continue...
         | 
| 103 | 
            +
                    pass
         | 
| 104 |  | 
| 105 | 
            +
                # 3. Check for a boolean value
         | 
| 106 | 
            +
                # This explicit check is important because bool('False') evaluates to True.
         | 
| 107 | 
            +
                s_lower = s.lower()
         | 
| 108 | 
            +
                if s_lower == 'true':
         | 
| 109 | 
            +
                    return True
         | 
| 110 | 
            +
                if s_lower == 'false':
         | 
| 111 | 
            +
                    return False
         | 
| 112 | 
            +
                    
         | 
| 113 | 
            +
                # 4. If nothing else worked, return the original string
         | 
| 114 | 
            +
                return s
         | 
| 115 |  | 
| 116 | 
             
            def handle_load_metadata(image_data: ImageMeta | None) -> List[Any]:
         | 
| 117 | 
             
                \"\"\"
         | 
|  | |
| 131 | 
             
                raw_values = transfer_metadata(output_fields, metadata, dataclass_fields)
         | 
| 132 |  | 
| 133 | 
             
                output_values = [gr.skip()] * len(output_fields)
         | 
| 134 | 
            +
                for i, (component, value) in enumerate(zip(output_fields, raw_values)):        
         | 
| 135 | 
             
                    if hasattr(component, 'root_label'):
         | 
| 136 | 
             
                        output_values[i] = create_dataclass_instance(PropertyConfig, value)
         | 
| 137 | 
             
                    else:
         | 
| 138 | 
            +
                        output_values[i] = gr.update(value=infer_type(value))
         | 
| 139 |  | 
| 140 | 
             
                return output_values
         | 
| 141 |  | 
|  | |
| 155 |  | 
| 156 | 
             
                params = list(inputs)
         | 
| 157 | 
             
                image_params = dict(zip(input_fields.keys(), params))
         | 
| 158 | 
            +
                #dataclass_fields = build_dataclass_fields(PropertyConfig)
         | 
| 159 | 
            +
                metadata = {label: image_params.get(label, "") for label in image_params.keys()}
         | 
| 160 |  | 
| 161 | 
             
                new_filepath = output_dir / "image_with_meta.png"
         | 
| 162 | 
            +
                
         | 
| 163 | 
             
                add_metadata(image_data, metadata, new_filepath)
         | 
| 164 |  | 
| 165 | 
             
                return str(new_filepath)
         | 
|  | |
| 184 | 
             
                        label="Upload Image (Custom metadata only)",
         | 
| 185 | 
             
                        type="filepath",
         | 
| 186 | 
             
                        width=300,
         | 
| 187 | 
            +
                        height=400,            
         | 
|  | |
| 188 | 
             
                        interactive=True
         | 
| 189 | 
             
                    )
         | 
| 190 | 
             
                    img_all = ImageMeta(
         | 
| 191 | 
             
                        label="Upload Image (All metadata)",
         | 
| 192 | 
             
                        only_custom_metadata=False,
         | 
| 193 | 
            +
                        type="filepath",
         | 
| 194 | 
             
                        width=300,
         | 
| 195 | 
            +
                        height=400,            
         | 
| 196 | 
             
                        popup_metadata_height=400,
         | 
| 197 | 
            +
                        popup_metadata_width=500,
         | 
| 198 | 
            +
                        interactive=True
         | 
| 199 | 
             
                    )
         | 
| 200 |  | 
| 201 | 
             
                gr.Markdown("## Metadata Viewer")
         | 
|  | |
| 221 | 
             
                with gr.Row():
         | 
| 222 | 
             
                    save_button = gr.Button("Add Metadata and Save Image")
         | 
| 223 | 
             
                    saved_file_output = gr.File(label="Download Image")
         | 
| 224 | 
            +
               
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 225 |  | 
| 226 | 
             
                input_fields = {
         | 
| 227 | 
             
                    "Model": model_box,
         | 
    	
        src/frontend/shared/ImagePreview.svelte
    CHANGED
    
    | @@ -355,7 +355,7 @@ | |
| 355 | 
             
                .load-metadata-button {
         | 
| 356 | 
             
                    margin-top: 1rem;
         | 
| 357 | 
             
                    padding: 0.5rem 1rem;
         | 
| 358 | 
            -
                    background-color: var(--button-primary- | 
| 359 | 
             
                    color: var(--button-primary-text-color);
         | 
| 360 | 
             
                    border: none;
         | 
| 361 | 
             
                    border-radius: 4px;
         | 
|  | |
| 355 | 
             
                .load-metadata-button {
         | 
| 356 | 
             
                    margin-top: 1rem;
         | 
| 357 | 
             
                    padding: 0.5rem 1rem;
         | 
| 358 | 
            +
                    background-color: var(--button-primary-border-color);
         | 
| 359 | 
             
                    color: var(--button-primary-text-color);
         | 
| 360 | 
             
                    border: none;
         | 
| 361 | 
             
                    border-radius: 4px;
         | 
    	
        src/frontend/shared/ImageUploader.svelte
    CHANGED
    
    | @@ -452,7 +452,7 @@ | |
| 452 | 
             
                .load-metadata-button {
         | 
| 453 | 
             
                    margin-top: 1rem;
         | 
| 454 | 
             
                    padding: 0.5rem 1rem;
         | 
| 455 | 
            -
                    background-color: var(--button-primary- | 
| 456 | 
             
                    color: var(--button-primary-text-color);
         | 
| 457 | 
             
                    border: none;
         | 
| 458 | 
             
                    border-radius: 4px;
         | 
|  | |
| 452 | 
             
                .load-metadata-button {
         | 
| 453 | 
             
                    margin-top: 1rem;
         | 
| 454 | 
             
                    padding: 0.5rem 1rem;
         | 
| 455 | 
            +
                    background-color: var(--button-primary-border-color);
         | 
| 456 | 
             
                    color: var(--button-primary-text-color);
         | 
| 457 | 
             
                    border: none;
         | 
| 458 | 
             
                    border-radius: 4px;
         | 
    	
        src/outputs/image_with_meta.png
    ADDED
    
    |   | 
| Git LFS Details
 | 
    	
        src/pyproject.toml
    CHANGED
    
    | @@ -8,7 +8,7 @@ build-backend = "hatchling.build" | |
| 8 |  | 
| 9 | 
             
            [project]
         | 
| 10 | 
             
            name = "gradio_imagemeta"
         | 
| 11 | 
            -
            version = "0.0. | 
| 12 | 
             
            description = "Image Preview with Metadata for Gradio Interface"
         | 
| 13 | 
             
            readme = "README.md"
         | 
| 14 | 
             
            license = "apache-2.0"
         | 
|  | |
| 8 |  | 
| 9 | 
             
            [project]
         | 
| 10 | 
             
            name = "gradio_imagemeta"
         | 
| 11 | 
            +
            version = "0.0.2"
         | 
| 12 | 
             
            description = "Image Preview with Metadata for Gradio Interface"
         | 
| 13 | 
             
            readme = "README.md"
         | 
| 14 | 
             
            license = "apache-2.0"
         | 
