Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- .gitattributes +3 -0
- .gitignore +15 -0
- README.md +673 -12
- __init__.py +0 -0
- app.py +216 -0
- css.css +157 -0
- requirements.txt +2 -0
- space.py +344 -0
- src/.gitignore +15 -0
- src/.gradio/cached_examples/24/Upload Image All metadata/7a159874ef63b94d8fa4/image_with_meta.png +3 -0
- src/.gradio/cached_examples/24/Upload Image Custom metadata only/8a0de8f0e921a79b67a8/image_with_meta.png +3 -0
- src/.gradio/cached_examples/24/indices.csv +1 -0
- src/.gradio/cached_examples/24/log.csv +2 -0
- src/.vscode/launch.json +29 -0
- src/README.md +673 -0
- src/backend/gradio_imagemeta/__init__.py +4 -0
- src/backend/gradio_imagemeta/helpers.py +182 -0
- src/backend/gradio_imagemeta/imagemeta.py +231 -0
- src/backend/gradio_imagemeta/templates/component/index.js +0 -0
- src/backend/gradio_imagemeta/templates/component/style.css +1 -0
- src/backend/gradio_imagemeta/templates/example/index.js +316 -0
- src/backend/gradio_imagemeta/templates/example/style.css +1 -0
- src/demo/__init__.py +0 -0
- src/demo/app.py +216 -0
- src/demo/css.css +157 -0
- src/demo/requirements.txt +2 -0
- src/demo/space.py +344 -0
- src/examples/image_with_meta.png +3 -0
- src/frontend/Example.svelte +49 -0
- src/frontend/Index.svelte +246 -0
- src/frontend/gradio.config.js +9 -0
- src/frontend/package-lock.json +0 -0
- src/frontend/package.json +58 -0
- src/frontend/shared/Image.svelte +44 -0
- src/frontend/shared/ImagePreview.svelte +369 -0
- src/frontend/shared/ImageUploader.svelte +466 -0
- src/frontend/shared/index.ts +2 -0
- src/frontend/shared/types.ts +5 -0
- src/frontend/shared/utils.ts +29 -0
- src/frontend/tsconfig.json +14 -0
- src/pyproject.toml +51 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* 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
|
.gitignore
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.eggs/
|
| 2 |
+
.vscode/
|
| 3 |
+
.gradio/
|
| 4 |
+
dist/
|
| 5 |
+
*.pyc
|
| 6 |
+
__pycache__/
|
| 7 |
+
*.py[cod]
|
| 8 |
+
*$py.class
|
| 9 |
+
__tmp/*
|
| 10 |
+
*.pyi
|
| 11 |
+
.mypycache
|
| 12 |
+
.ruff_cache
|
| 13 |
+
node_modules
|
| 14 |
+
backend/**/templates/
|
| 15 |
+
outputs/
|
README.md
CHANGED
|
@@ -1,12 +1,673 @@
|
|
| 1 |
-
---
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
---
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
tags: [gradio-custom-component, Image]
|
| 3 |
+
title: gradio_imagemeta
|
| 4 |
+
short_description: Image Preview with Metadata for Gradio Interface
|
| 5 |
+
colorFrom: blue
|
| 6 |
+
colorTo: yellow
|
| 7 |
+
sdk: gradio
|
| 8 |
+
pinned: false
|
| 9 |
+
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-%20orange">
|
| 14 |
+
|
| 15 |
+
Image Preview with Metadata for Gradio Interface
|
| 16 |
+
|
| 17 |
+
## Installation
|
| 18 |
+
|
| 19 |
+
```bash
|
| 20 |
+
pip install gradio_imagemeta
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
## Usage
|
| 24 |
+
|
| 25 |
+
```python
|
| 26 |
+
from dataclasses import dataclass, field
|
| 27 |
+
from typing import List, Any
|
| 28 |
+
import gradio as gr
|
| 29 |
+
from gradio_imagemeta import ImageMeta
|
| 30 |
+
from gradio_imagemeta.helpers import extract_metadata, add_metadata, transfer_metadata
|
| 31 |
+
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 |
+
|
| 38 |
+
@dataclass
|
| 39 |
+
class ImageSettings:
|
| 40 |
+
"""Configuration for image metadata settings."""
|
| 41 |
+
model: str = field(default="", metadata={"label": "Model"})
|
| 42 |
+
f_number: str = field(default="", metadata={"label": "FNumber"})
|
| 43 |
+
iso_speed_ratings: str = field(default="", metadata={"label": "ISOSpeedRatings"})
|
| 44 |
+
s_churn: float = field(
|
| 45 |
+
default=0.0,
|
| 46 |
+
metadata={"component": "slider", "label": "Schurn", "minimum": 0.0, "maximum": 1.0, "step": 0.01},
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
@dataclass
|
| 50 |
+
class PropertyConfig:
|
| 51 |
+
"""Root configuration for image properties, including nested image settings."""
|
| 52 |
+
image_settings: ImageSettings = field(default_factory=ImageSettings)
|
| 53 |
+
description: str = field(default="", metadata={"label": "Description"})
|
| 54 |
+
|
| 55 |
+
def process_example_images(img_custom_path: str, img_all_path: str) -> tuple[str, str]:
|
| 56 |
+
"""
|
| 57 |
+
Processes example image paths for display in ImageMeta components.
|
| 58 |
+
|
| 59 |
+
Args:
|
| 60 |
+
img_custom_path: File path for the image to display in img_custom.
|
| 61 |
+
img_all_path: File path for the image to display in img_all.
|
| 62 |
+
|
| 63 |
+
Returns:
|
| 64 |
+
Tuple of file paths for img_custom and img_all outputs.
|
| 65 |
+
"""
|
| 66 |
+
# Verify file existence
|
| 67 |
+
if not Path(img_custom_path).is_file():
|
| 68 |
+
raise FileNotFoundError(f"Image not found: {img_custom_path}")
|
| 69 |
+
if not Path(img_all_path).is_file():
|
| 70 |
+
raise FileNotFoundError(f"Image not found: {img_all_path}")
|
| 71 |
+
|
| 72 |
+
# Return file paths as strings (ImageMeta accepts file paths as input)
|
| 73 |
+
return str(img_custom_path), str(img_all_path)
|
| 74 |
+
|
| 75 |
+
def handle_load_metadata(image_data: ImageMeta | None) -> List[Any]:
|
| 76 |
+
"""
|
| 77 |
+
Processes image metadata and maps it to output components.
|
| 78 |
+
|
| 79 |
+
Args:
|
| 80 |
+
image_data: ImageMeta object containing image data and metadata, or None.
|
| 81 |
+
|
| 82 |
+
Returns:
|
| 83 |
+
A list of values for output components (Textbox, Slider, or PropertySheet instances).
|
| 84 |
+
"""
|
| 85 |
+
if not image_data:
|
| 86 |
+
return [gr.Textbox(value="") for _ in output_fields]
|
| 87 |
+
|
| 88 |
+
metadata = extract_metadata(image_data, only_custom_metadata=True)
|
| 89 |
+
dataclass_fields = build_dataclass_fields(PropertyConfig)
|
| 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.Textbox(value=value)
|
| 98 |
+
|
| 99 |
+
return output_values
|
| 100 |
+
|
| 101 |
+
def save_image_with_metadata(image_data: Any, *inputs: Any) -> str | None:
|
| 102 |
+
"""
|
| 103 |
+
Saves an image with updated metadata to a file.
|
| 104 |
+
|
| 105 |
+
Args:
|
| 106 |
+
image_data: Input image data (e.g., file path or PIL Image).
|
| 107 |
+
*inputs: Variable number of input values from UI components (Textbox, Slider).
|
| 108 |
+
|
| 109 |
+
Returns:
|
| 110 |
+
The file path of the saved image, or None if no image is provided.
|
| 111 |
+
"""
|
| 112 |
+
if not image_data:
|
| 113 |
+
return 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 dataclass_fields.keys()}
|
| 119 |
+
|
| 120 |
+
new_filepath = output_dir / "image_with_meta.png"
|
| 121 |
+
add_metadata(image_data, metadata, new_filepath)
|
| 122 |
+
|
| 123 |
+
return str(new_filepath)
|
| 124 |
+
|
| 125 |
+
initial_property_from_meta_config = PropertyConfig()
|
| 126 |
+
|
| 127 |
+
with gr.Blocks() as demo:
|
| 128 |
+
gr.Markdown("# ImageMeta Component Demo")
|
| 129 |
+
gr.Markdown(
|
| 130 |
+
"""
|
| 131 |
+
**To Test:**
|
| 132 |
+
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.
|
| 133 |
+
2. Click the 'Info' icon (ⓘ) in the top-left of the image component to view the metadata panel.
|
| 134 |
+
3. Click 'Load Metadata' in the popup to populate the fields below with metadata values (`Model`, `FNumber`, `ISOSpeedRatings`, `Schurn`, `Description`).
|
| 135 |
+
4. The section below displays how metadata is rendered in components and the `PropertySheet` custom component, showing the hierarchical structure of the image settings.
|
| 136 |
+
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.
|
| 137 |
+
"""
|
| 138 |
+
)
|
| 139 |
+
property_sheet_state = gr.State(value=initial_property_from_meta_config)
|
| 140 |
+
with gr.Row():
|
| 141 |
+
img_custom = ImageMeta(
|
| 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")
|
| 159 |
+
gr.Markdown("### Individual Components")
|
| 160 |
+
with gr.Row():
|
| 161 |
+
model_box = gr.Textbox(label="Model")
|
| 162 |
+
fnumber_box = gr.Textbox(label="FNumber")
|
| 163 |
+
iso_box = gr.Textbox(label="ISOSpeedRatings")
|
| 164 |
+
s_churn = gr.Slider(label="Schurn", value=1.0, minimum=0.0, maximum=1.0, step=0.1)
|
| 165 |
+
description_box = gr.Textbox(label="Description")
|
| 166 |
+
|
| 167 |
+
gr.Markdown("### PropertySheet Component")
|
| 168 |
+
with gr.Row():
|
| 169 |
+
property_sheet = PropertySheet(
|
| 170 |
+
value=initial_property_from_meta_config,
|
| 171 |
+
label="Image Settings",
|
| 172 |
+
width=400,
|
| 173 |
+
height=550,
|
| 174 |
+
visible=True,
|
| 175 |
+
root_label="General"
|
| 176 |
+
)
|
| 177 |
+
gr.Markdown("## Metadata Editor")
|
| 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,
|
| 195 |
+
"FNumber": fnumber_box,
|
| 196 |
+
"ISOSpeedRatings": iso_box,
|
| 197 |
+
"Schurn": s_churn,
|
| 198 |
+
"Description": description_box
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
output_fields = [
|
| 202 |
+
property_sheet,
|
| 203 |
+
model_box,
|
| 204 |
+
fnumber_box,
|
| 205 |
+
iso_box,
|
| 206 |
+
s_churn,
|
| 207 |
+
description_box
|
| 208 |
+
]
|
| 209 |
+
|
| 210 |
+
img_custom.load_metadata(handle_load_metadata, inputs=img_custom, outputs=output_fields)
|
| 211 |
+
img_all.load_metadata(handle_load_metadata, inputs=img_all, outputs=output_fields)
|
| 212 |
+
|
| 213 |
+
def handle_render_change(updated_config: PropertyConfig, current_state: PropertyConfig):
|
| 214 |
+
"""
|
| 215 |
+
Updates the PropertySheet state when its configuration changes.
|
| 216 |
+
|
| 217 |
+
Args:
|
| 218 |
+
updated_config: The new PropertyConfig instance from the PropertySheet.
|
| 219 |
+
current_state: The current PropertyConfig state.
|
| 220 |
+
|
| 221 |
+
Returns:
|
| 222 |
+
A tuple of (updated_config, updated_config) or (current_state, current_state) if updated_config is None.
|
| 223 |
+
"""
|
| 224 |
+
if updated_config is None:
|
| 225 |
+
return current_state, current_state
|
| 226 |
+
return updated_config, updated_config
|
| 227 |
+
|
| 228 |
+
property_sheet.change(
|
| 229 |
+
fn=handle_render_change,
|
| 230 |
+
inputs=[property_sheet, property_sheet_state],
|
| 231 |
+
outputs=[property_sheet, property_sheet_state]
|
| 232 |
+
)
|
| 233 |
+
save_button.click(
|
| 234 |
+
save_image_with_metadata,
|
| 235 |
+
inputs=[img_custom, *input_fields.values()],
|
| 236 |
+
outputs=[saved_file_output]
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
if __name__ == "__main__":
|
| 240 |
+
demo.launch()
|
| 241 |
+
```
|
| 242 |
+
|
| 243 |
+
## `ImageMeta`
|
| 244 |
+
|
| 245 |
+
### Initialization
|
| 246 |
+
|
| 247 |
+
<table>
|
| 248 |
+
<thead>
|
| 249 |
+
<tr>
|
| 250 |
+
<th align="left">name</th>
|
| 251 |
+
<th align="left" style="width: 25%;">type</th>
|
| 252 |
+
<th align="left">default</th>
|
| 253 |
+
<th align="left">description</th>
|
| 254 |
+
</tr>
|
| 255 |
+
</thead>
|
| 256 |
+
<tbody>
|
| 257 |
+
<tr>
|
| 258 |
+
<td align="left"><code>value</code></td>
|
| 259 |
+
<td align="left" style="width: 25%;">
|
| 260 |
+
|
| 261 |
+
```python
|
| 262 |
+
str | Image.Image | np.ndarray | Callable | None
|
| 263 |
+
```
|
| 264 |
+
|
| 265 |
+
</td>
|
| 266 |
+
<td align="left"><code>None</code></td>
|
| 267 |
+
<td align="left">A PIL Image, numpy array, path or URL for the default value that Image component is going to take. If a function is provided, the function will be called each time the app loads to set the initial value of this component.</td>
|
| 268 |
+
</tr>
|
| 269 |
+
|
| 270 |
+
<tr>
|
| 271 |
+
<td align="left"><code>format</code></td>
|
| 272 |
+
<td align="left" style="width: 25%;">
|
| 273 |
+
|
| 274 |
+
```python
|
| 275 |
+
str
|
| 276 |
+
```
|
| 277 |
+
|
| 278 |
+
</td>
|
| 279 |
+
<td align="left"><code>"webp"</code></td>
|
| 280 |
+
<td align="left">File format (e.g. "png" or "gif"). Used to save image if it does not already have a valid format (e.g. if the image is being returned to the frontend as a numpy array or PIL Image). The format should be supported by the PIL library. Applies both when this component is used as an input or output. This parameter has no effect on SVG files.</td>
|
| 281 |
+
</tr>
|
| 282 |
+
|
| 283 |
+
<tr>
|
| 284 |
+
<td align="left"><code>height</code></td>
|
| 285 |
+
<td align="left" style="width: 25%;">
|
| 286 |
+
|
| 287 |
+
```python
|
| 288 |
+
int | str | None
|
| 289 |
+
```
|
| 290 |
+
|
| 291 |
+
</td>
|
| 292 |
+
<td align="left"><code>None</code></td>
|
| 293 |
+
<td align="left">The height of the component, specified in pixels if a number is passed, or in CSS units if a string is passed. This has no effect on the preprocessed image file or numpy array, but will affect the displayed image.</td>
|
| 294 |
+
</tr>
|
| 295 |
+
|
| 296 |
+
<tr>
|
| 297 |
+
<td align="left"><code>width</code></td>
|
| 298 |
+
<td align="left" style="width: 25%;">
|
| 299 |
+
|
| 300 |
+
```python
|
| 301 |
+
int | str | None
|
| 302 |
+
```
|
| 303 |
+
|
| 304 |
+
</td>
|
| 305 |
+
<td align="left"><code>None</code></td>
|
| 306 |
+
<td align="left">The width of the component, specified in pixels if a number is passed, or in CSS units if a string is passed. This has no effect on the preprocessed image file or numpy array, but will affect the displayed image.</td>
|
| 307 |
+
</tr>
|
| 308 |
+
|
| 309 |
+
<tr>
|
| 310 |
+
<td align="left"><code>image_mode</code></td>
|
| 311 |
+
<td align="left" style="width: 25%;">
|
| 312 |
+
|
| 313 |
+
```python
|
| 314 |
+
Literal[
|
| 315 |
+
"1",
|
| 316 |
+
"L",
|
| 317 |
+
"P",
|
| 318 |
+
"RGB",
|
| 319 |
+
"RGBA",
|
| 320 |
+
"CMYK",
|
| 321 |
+
"YCbCr",
|
| 322 |
+
"LAB",
|
| 323 |
+
"HSV",
|
| 324 |
+
"I",
|
| 325 |
+
"F",
|
| 326 |
+
]
|
| 327 |
+
| None
|
| 328 |
+
```
|
| 329 |
+
|
| 330 |
+
</td>
|
| 331 |
+
<td align="left"><code>"RGB"</code></td>
|
| 332 |
+
<td align="left">The pixel format and color depth that the image should be loaded and preprocessed as. "RGB" will load the image as a color image, or "L" as black-and-white. See https://pillow.readthedocs.io/en/stable/handbook/concepts.html for other supported image modes and their meaning. This parameter has no effect on SVG or GIF files. If set to None, the image_mode will be inferred from the image file type (e.g. "RGBA" for a .png image, "RGB" in most other cases).</td>
|
| 333 |
+
</tr>
|
| 334 |
+
|
| 335 |
+
<tr>
|
| 336 |
+
<td align="left"><code>type</code></td>
|
| 337 |
+
<td align="left" style="width: 25%;">
|
| 338 |
+
|
| 339 |
+
```python
|
| 340 |
+
Literal["numpy", "pil", "filepath"]
|
| 341 |
+
```
|
| 342 |
+
|
| 343 |
+
</td>
|
| 344 |
+
<td align="left"><code>"numpy"</code></td>
|
| 345 |
+
<td align="left">The format the image is converted before being passed into the prediction function. "numpy" converts the image to a numpy array with shape (height, width, 3) and values from 0 to 255, "pil" converts the image to a PIL image object, "filepath" passes a str path to a temporary file containing the image. To support animated GIFs in input, the `type` should be set to "filepath" or "pil". To support SVGs, the `type` should be set to "filepath".</td>
|
| 346 |
+
</tr>
|
| 347 |
+
|
| 348 |
+
<tr>
|
| 349 |
+
<td align="left"><code>label</code></td>
|
| 350 |
+
<td align="left" style="width: 25%;">
|
| 351 |
+
|
| 352 |
+
```python
|
| 353 |
+
str | I18nData | None
|
| 354 |
+
```
|
| 355 |
+
|
| 356 |
+
</td>
|
| 357 |
+
<td align="left"><code>None</code></td>
|
| 358 |
+
<td align="left">The label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.</td>
|
| 359 |
+
</tr>
|
| 360 |
+
|
| 361 |
+
<tr>
|
| 362 |
+
<td align="left"><code>every</code></td>
|
| 363 |
+
<td align="left" style="width: 25%;">
|
| 364 |
+
|
| 365 |
+
```python
|
| 366 |
+
Timer | float | None
|
| 367 |
+
```
|
| 368 |
+
|
| 369 |
+
</td>
|
| 370 |
+
<td align="left"><code>None</code></td>
|
| 371 |
+
<td align="left">Continously calls `value` to recalculate it if `value` is a function (has no effect otherwise). Can provide a Timer whose tick resets `value`, or a float that provides the regular interval for the reset Timer.</td>
|
| 372 |
+
</tr>
|
| 373 |
+
|
| 374 |
+
<tr>
|
| 375 |
+
<td align="left"><code>inputs</code></td>
|
| 376 |
+
<td align="left" style="width: 25%;">
|
| 377 |
+
|
| 378 |
+
```python
|
| 379 |
+
Component | Sequence[Component] | set[Component] | None
|
| 380 |
+
```
|
| 381 |
+
|
| 382 |
+
</td>
|
| 383 |
+
<td align="left"><code>None</code></td>
|
| 384 |
+
<td align="left">Components that are used as inputs to calculate `value` if `value` is a function (has no effect otherwise). `value` is recalculated any time the inputs change.</td>
|
| 385 |
+
</tr>
|
| 386 |
+
|
| 387 |
+
<tr>
|
| 388 |
+
<td align="left"><code>show_label</code></td>
|
| 389 |
+
<td align="left" style="width: 25%;">
|
| 390 |
+
|
| 391 |
+
```python
|
| 392 |
+
bool | None
|
| 393 |
+
```
|
| 394 |
+
|
| 395 |
+
</td>
|
| 396 |
+
<td align="left"><code>None</code></td>
|
| 397 |
+
<td align="left">If True, will display label.</td>
|
| 398 |
+
</tr>
|
| 399 |
+
|
| 400 |
+
<tr>
|
| 401 |
+
<td align="left"><code>show_download_button</code></td>
|
| 402 |
+
<td align="left" style="width: 25%;">
|
| 403 |
+
|
| 404 |
+
```python
|
| 405 |
+
bool
|
| 406 |
+
```
|
| 407 |
+
|
| 408 |
+
</td>
|
| 409 |
+
<td align="left"><code>True</code></td>
|
| 410 |
+
<td align="left">If True, will display button to download image. Only applies if interactive is False (e.g. if the component is used as an output).</td>
|
| 411 |
+
</tr>
|
| 412 |
+
|
| 413 |
+
<tr>
|
| 414 |
+
<td align="left"><code>container</code></td>
|
| 415 |
+
<td align="left" style="width: 25%;">
|
| 416 |
+
|
| 417 |
+
```python
|
| 418 |
+
bool
|
| 419 |
+
```
|
| 420 |
+
|
| 421 |
+
</td>
|
| 422 |
+
<td align="left"><code>True</code></td>
|
| 423 |
+
<td align="left">If True, will place the component in a container - providing some extra padding around the border.</td>
|
| 424 |
+
</tr>
|
| 425 |
+
|
| 426 |
+
<tr>
|
| 427 |
+
<td align="left"><code>scale</code></td>
|
| 428 |
+
<td align="left" style="width: 25%;">
|
| 429 |
+
|
| 430 |
+
```python
|
| 431 |
+
int | None
|
| 432 |
+
```
|
| 433 |
+
|
| 434 |
+
</td>
|
| 435 |
+
<td align="left"><code>None</code></td>
|
| 436 |
+
<td align="left">Relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.</td>
|
| 437 |
+
</tr>
|
| 438 |
+
|
| 439 |
+
<tr>
|
| 440 |
+
<td align="left"><code>min_width</code></td>
|
| 441 |
+
<td align="left" style="width: 25%;">
|
| 442 |
+
|
| 443 |
+
```python
|
| 444 |
+
int
|
| 445 |
+
```
|
| 446 |
+
|
| 447 |
+
</td>
|
| 448 |
+
<td align="left"><code>160</code></td>
|
| 449 |
+
<td align="left">Minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.</td>
|
| 450 |
+
</tr>
|
| 451 |
+
|
| 452 |
+
<tr>
|
| 453 |
+
<td align="left"><code>interactive</code></td>
|
| 454 |
+
<td align="left" style="width: 25%;">
|
| 455 |
+
|
| 456 |
+
```python
|
| 457 |
+
bool | None
|
| 458 |
+
```
|
| 459 |
+
|
| 460 |
+
</td>
|
| 461 |
+
<td align="left"><code>None</code></td>
|
| 462 |
+
<td align="left">If True, will allow users to upload and edit an image; if False, can only be used to display images. If not provided, this is inferred based on whether the component is used as an input or output.</td>
|
| 463 |
+
</tr>
|
| 464 |
+
|
| 465 |
+
<tr>
|
| 466 |
+
<td align="left"><code>visible</code></td>
|
| 467 |
+
<td align="left" style="width: 25%;">
|
| 468 |
+
|
| 469 |
+
```python
|
| 470 |
+
bool
|
| 471 |
+
```
|
| 472 |
+
|
| 473 |
+
</td>
|
| 474 |
+
<td align="left"><code>True</code></td>
|
| 475 |
+
<td align="left">If False, component will be hidden.</td>
|
| 476 |
+
</tr>
|
| 477 |
+
|
| 478 |
+
<tr>
|
| 479 |
+
<td align="left"><code>elem_id</code></td>
|
| 480 |
+
<td align="left" style="width: 25%;">
|
| 481 |
+
|
| 482 |
+
```python
|
| 483 |
+
str | None
|
| 484 |
+
```
|
| 485 |
+
|
| 486 |
+
</td>
|
| 487 |
+
<td align="left"><code>None</code></td>
|
| 488 |
+
<td align="left">An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.</td>
|
| 489 |
+
</tr>
|
| 490 |
+
|
| 491 |
+
<tr>
|
| 492 |
+
<td align="left"><code>elem_classes</code></td>
|
| 493 |
+
<td align="left" style="width: 25%;">
|
| 494 |
+
|
| 495 |
+
```python
|
| 496 |
+
list[str] | str | None
|
| 497 |
+
```
|
| 498 |
+
|
| 499 |
+
</td>
|
| 500 |
+
<td align="left"><code>None</code></td>
|
| 501 |
+
<td align="left">An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.</td>
|
| 502 |
+
</tr>
|
| 503 |
+
|
| 504 |
+
<tr>
|
| 505 |
+
<td align="left"><code>render</code></td>
|
| 506 |
+
<td align="left" style="width: 25%;">
|
| 507 |
+
|
| 508 |
+
```python
|
| 509 |
+
bool
|
| 510 |
+
```
|
| 511 |
+
|
| 512 |
+
</td>
|
| 513 |
+
<td align="left"><code>True</code></td>
|
| 514 |
+
<td align="left">If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.</td>
|
| 515 |
+
</tr>
|
| 516 |
+
|
| 517 |
+
<tr>
|
| 518 |
+
<td align="left"><code>key</code></td>
|
| 519 |
+
<td align="left" style="width: 25%;">
|
| 520 |
+
|
| 521 |
+
```python
|
| 522 |
+
int | str | tuple[int | str, ...] | None
|
| 523 |
+
```
|
| 524 |
+
|
| 525 |
+
</td>
|
| 526 |
+
<td align="left"><code>None</code></td>
|
| 527 |
+
<td align="left">In a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render.</td>
|
| 528 |
+
</tr>
|
| 529 |
+
|
| 530 |
+
<tr>
|
| 531 |
+
<td align="left"><code>preserved_by_key</code></td>
|
| 532 |
+
<td align="left" style="width: 25%;">
|
| 533 |
+
|
| 534 |
+
```python
|
| 535 |
+
list[str] | str | None
|
| 536 |
+
```
|
| 537 |
+
|
| 538 |
+
</td>
|
| 539 |
+
<td align="left"><code>"value"</code></td>
|
| 540 |
+
<td align="left">A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor.</td>
|
| 541 |
+
</tr>
|
| 542 |
+
|
| 543 |
+
<tr>
|
| 544 |
+
<td align="left"><code>show_share_button</code></td>
|
| 545 |
+
<td align="left" style="width: 25%;">
|
| 546 |
+
|
| 547 |
+
```python
|
| 548 |
+
bool | None
|
| 549 |
+
```
|
| 550 |
+
|
| 551 |
+
</td>
|
| 552 |
+
<td align="left"><code>None</code></td>
|
| 553 |
+
<td align="left">If True, will show a share icon in the corner of the component that allows user to share outputs to Hugging Face Spaces Discussions. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.</td>
|
| 554 |
+
</tr>
|
| 555 |
+
|
| 556 |
+
<tr>
|
| 557 |
+
<td align="left"><code>placeholder</code></td>
|
| 558 |
+
<td align="left" style="width: 25%;">
|
| 559 |
+
|
| 560 |
+
```python
|
| 561 |
+
str | None
|
| 562 |
+
```
|
| 563 |
+
|
| 564 |
+
</td>
|
| 565 |
+
<td align="left"><code>None</code></td>
|
| 566 |
+
<td align="left">Custom text for the upload area. Overrides default upload messages when provided. Accepts new lines and `#` to designate a heading.</td>
|
| 567 |
+
</tr>
|
| 568 |
+
|
| 569 |
+
<tr>
|
| 570 |
+
<td align="left"><code>show_fullscreen_button</code></td>
|
| 571 |
+
<td align="left" style="width: 25%;">
|
| 572 |
+
|
| 573 |
+
```python
|
| 574 |
+
bool
|
| 575 |
+
```
|
| 576 |
+
|
| 577 |
+
</td>
|
| 578 |
+
<td align="left"><code>True</code></td>
|
| 579 |
+
<td align="left">If True, will show a fullscreen icon in the corner of the component that allows user to view the image in fullscreen mode. If False, icon does not appear.</td>
|
| 580 |
+
</tr>
|
| 581 |
+
|
| 582 |
+
<tr>
|
| 583 |
+
<td align="left"><code>only_custom_metadata</code></td>
|
| 584 |
+
<td align="left" style="width: 25%;">
|
| 585 |
+
|
| 586 |
+
```python
|
| 587 |
+
bool
|
| 588 |
+
```
|
| 589 |
+
|
| 590 |
+
</td>
|
| 591 |
+
<td align="left"><code>True</code></td>
|
| 592 |
+
<td align="left">If True, extracts only custom metadata, excluding technical metadata like ImageWidth or ImageHeight. Defaults to True.</td>
|
| 593 |
+
</tr>
|
| 594 |
+
|
| 595 |
+
<tr>
|
| 596 |
+
<td align="left"><code>disable_preprocess</code></td>
|
| 597 |
+
<td align="left" style="width: 25%;">
|
| 598 |
+
|
| 599 |
+
```python
|
| 600 |
+
bool
|
| 601 |
+
```
|
| 602 |
+
|
| 603 |
+
</td>
|
| 604 |
+
<td align="left"><code>True</code></td>
|
| 605 |
+
<td align="left">If True, skips preprocessing and returns the raw ImageMetaData payload. Defaults to True.</td>
|
| 606 |
+
</tr>
|
| 607 |
+
|
| 608 |
+
<tr>
|
| 609 |
+
<td align="left"><code>popup_metadata_width</code></td>
|
| 610 |
+
<td align="left" style="width: 25%;">
|
| 611 |
+
|
| 612 |
+
```python
|
| 613 |
+
int | str
|
| 614 |
+
```
|
| 615 |
+
|
| 616 |
+
</td>
|
| 617 |
+
<td align="left"><code>400</code></td>
|
| 618 |
+
<td align="left">Metadata popup width in pixels or CSS units. Defaults to 400.</td>
|
| 619 |
+
</tr>
|
| 620 |
+
|
| 621 |
+
<tr>
|
| 622 |
+
<td align="left"><code>popup_metadata_height</code></td>
|
| 623 |
+
<td align="left" style="width: 25%;">
|
| 624 |
+
|
| 625 |
+
```python
|
| 626 |
+
int | str
|
| 627 |
+
```
|
| 628 |
+
|
| 629 |
+
</td>
|
| 630 |
+
<td align="left"><code>300</code></td>
|
| 631 |
+
<td align="left">Metadata popup height in pixels or CSS units. Defaults to 300.</td>
|
| 632 |
+
</tr>
|
| 633 |
+
</tbody></table>
|
| 634 |
+
|
| 635 |
+
|
| 636 |
+
### Events
|
| 637 |
+
|
| 638 |
+
| name | description |
|
| 639 |
+
|:-----|:------------|
|
| 640 |
+
| `clear` | This listener is triggered when the user clears the ImageMeta using the clear button for the component. |
|
| 641 |
+
| `change` | Triggered when the value of the ImageMeta changes either because of user input (e.g. a user types in a textbox) OR because of a function update (e.g. an image receives a value from the output of an event trigger). See `.input()` for a listener that is only triggered by user input. |
|
| 642 |
+
| `select` | Event listener for when the user selects or deselects the ImageMeta. Uses event data gradio.SelectData to carry `value` referring to the label of the ImageMeta, and `selected` to refer to state of the ImageMeta. See EventData documentation on how to use this event data |
|
| 643 |
+
| `upload` | This listener is triggered when the user uploads a file into the ImageMeta. |
|
| 644 |
+
| `input` | This listener is triggered when the user changes the value of the ImageMeta. |
|
| 645 |
+
| `load_metadata` | Triggered when the user clicks the 'Load Metadata' button, expecting ImageMetaData as input. |
|
| 646 |
+
|
| 647 |
+
|
| 648 |
+
|
| 649 |
+
### User function
|
| 650 |
+
|
| 651 |
+
The impact on the users predict function varies depending on whether the component is used as an input or output for an event (or both).
|
| 652 |
+
|
| 653 |
+
- When used as an Input, the component only impacts the input signature of the user function.
|
| 654 |
+
- When used as an output, the component only impacts the return signature of the user function.
|
| 655 |
+
|
| 656 |
+
The code snippet below is accurate in cases where the component is used as both an input and an output.
|
| 657 |
+
|
| 658 |
+
- **As output:** Is passed, preprocessed image as a NumPy array, PIL Image, filepath, ImageMetaData, or None.
|
| 659 |
+
- **As input:** Should return, input image as a NumPy array, PIL Image, string (file path or URL), Path object, or None.
|
| 660 |
+
|
| 661 |
+
```python
|
| 662 |
+
def predict(
|
| 663 |
+
value: numpy.ndarray | PIL.Image.Image | str | ImageMetaData | None
|
| 664 |
+
) -> numpy.ndarray | PIL.Image.Image | str | pathlib.Path | None:
|
| 665 |
+
return value
|
| 666 |
+
```
|
| 667 |
+
|
| 668 |
+
|
| 669 |
+
## `ImageMetaData`
|
| 670 |
+
```python
|
| 671 |
+
class ImageMetaData(ImageData):
|
| 672 |
+
pass
|
| 673 |
+
```
|
__init__.py
ADDED
|
File without changes
|
app.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
output_dir = Path("outputs")
|
| 11 |
+
output_dir.mkdir(exist_ok=True)
|
| 12 |
+
|
| 13 |
+
@dataclass
|
| 14 |
+
class ImageSettings:
|
| 15 |
+
"""Configuration for image metadata settings."""
|
| 16 |
+
model: str = field(default="", metadata={"label": "Model"})
|
| 17 |
+
f_number: str = field(default="", metadata={"label": "FNumber"})
|
| 18 |
+
iso_speed_ratings: str = field(default="", metadata={"label": "ISOSpeedRatings"})
|
| 19 |
+
s_churn: float = field(
|
| 20 |
+
default=0.0,
|
| 21 |
+
metadata={"component": "slider", "label": "Schurn", "minimum": 0.0, "maximum": 1.0, "step": 0.01},
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
@dataclass
|
| 25 |
+
class PropertyConfig:
|
| 26 |
+
"""Root configuration for image properties, including nested image settings."""
|
| 27 |
+
image_settings: ImageSettings = field(default_factory=ImageSettings)
|
| 28 |
+
description: str = field(default="", metadata={"label": "Description"})
|
| 29 |
+
|
| 30 |
+
def process_example_images(img_custom_path: str, img_all_path: str) -> tuple[str, str]:
|
| 31 |
+
"""
|
| 32 |
+
Processes example image paths for display in ImageMeta components.
|
| 33 |
+
|
| 34 |
+
Args:
|
| 35 |
+
img_custom_path: File path for the image to display in img_custom.
|
| 36 |
+
img_all_path: File path for the image to display in img_all.
|
| 37 |
+
|
| 38 |
+
Returns:
|
| 39 |
+
Tuple of file paths for img_custom and img_all outputs.
|
| 40 |
+
"""
|
| 41 |
+
# Verify file existence
|
| 42 |
+
if not Path(img_custom_path.path).is_file():
|
| 43 |
+
raise FileNotFoundError(f"Image not found: {img_custom_path}")
|
| 44 |
+
if not Path(img_all_path.path).is_file():
|
| 45 |
+
raise FileNotFoundError(f"Image not found: {img_all_path}")
|
| 46 |
+
|
| 47 |
+
# Return file paths as strings (ImageMeta accepts file paths as input)
|
| 48 |
+
return img_custom_path.path, img_all_path.path
|
| 49 |
+
|
| 50 |
+
def handle_load_metadata(image_data: ImageMeta | None) -> List[Any]:
|
| 51 |
+
"""
|
| 52 |
+
Processes image metadata and maps it to output components.
|
| 53 |
+
|
| 54 |
+
Args:
|
| 55 |
+
image_data: ImageMeta object containing image data and metadata, or None.
|
| 56 |
+
|
| 57 |
+
Returns:
|
| 58 |
+
A list of values for output components (Textbox, Slider, or PropertySheet instances).
|
| 59 |
+
"""
|
| 60 |
+
if not image_data:
|
| 61 |
+
return [gr.Textbox(value="") for _ in output_fields]
|
| 62 |
+
|
| 63 |
+
metadata = extract_metadata(image_data, only_custom_metadata=True)
|
| 64 |
+
dataclass_fields = build_dataclass_fields(PropertyConfig)
|
| 65 |
+
raw_values = transfer_metadata(output_fields, metadata, dataclass_fields)
|
| 66 |
+
|
| 67 |
+
output_values = [gr.skip()] * len(output_fields)
|
| 68 |
+
for i, (component, value) in enumerate(zip(output_fields, raw_values)):
|
| 69 |
+
if hasattr(component, 'root_label'):
|
| 70 |
+
output_values[i] = create_dataclass_instance(PropertyConfig, value)
|
| 71 |
+
else:
|
| 72 |
+
output_values[i] = gr.Textbox(value=value)
|
| 73 |
+
|
| 74 |
+
return output_values
|
| 75 |
+
|
| 76 |
+
def save_image_with_metadata(image_data: Any, *inputs: Any) -> str | None:
|
| 77 |
+
"""
|
| 78 |
+
Saves an image with updated metadata to a file.
|
| 79 |
+
|
| 80 |
+
Args:
|
| 81 |
+
image_data: Input image data (e.g., file path or PIL Image).
|
| 82 |
+
*inputs: Variable number of input values from UI components (Textbox, Slider).
|
| 83 |
+
|
| 84 |
+
Returns:
|
| 85 |
+
The file path of the saved image, or None if no image is provided.
|
| 86 |
+
"""
|
| 87 |
+
if not image_data:
|
| 88 |
+
return None
|
| 89 |
+
|
| 90 |
+
params = list(inputs)
|
| 91 |
+
image_params = dict(zip(input_fields.keys(), params))
|
| 92 |
+
dataclass_fields = build_dataclass_fields(PropertyConfig)
|
| 93 |
+
metadata = {label: image_params.get(label, "") for label in dataclass_fields.keys()}
|
| 94 |
+
|
| 95 |
+
new_filepath = output_dir / "image_with_meta.png"
|
| 96 |
+
add_metadata(image_data, metadata, new_filepath)
|
| 97 |
+
|
| 98 |
+
return str(new_filepath)
|
| 99 |
+
|
| 100 |
+
initial_property_from_meta_config = PropertyConfig()
|
| 101 |
+
|
| 102 |
+
with gr.Blocks() as demo:
|
| 103 |
+
gr.Markdown("# ImageMeta Component Demo")
|
| 104 |
+
gr.Markdown(
|
| 105 |
+
"""
|
| 106 |
+
**To Test:**
|
| 107 |
+
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.
|
| 108 |
+
2. Click the 'Info' icon (ⓘ) in the top-left of the image component to view the metadata panel.
|
| 109 |
+
3. Click 'Load Metadata' in the popup to populate the fields below with metadata values (`Model`, `FNumber`, `ISOSpeedRatings`, `Schurn`, `Description`).
|
| 110 |
+
4. The section below displays how metadata is rendered in components and the `PropertySheet` custom component, showing the hierarchical structure of the image settings.
|
| 111 |
+
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.
|
| 112 |
+
"""
|
| 113 |
+
)
|
| 114 |
+
property_sheet_state = gr.State(value=initial_property_from_meta_config)
|
| 115 |
+
with gr.Row():
|
| 116 |
+
img_custom = ImageMeta(
|
| 117 |
+
label="Upload Image (Custom metadata only)",
|
| 118 |
+
type="filepath",
|
| 119 |
+
width=300,
|
| 120 |
+
height=400,
|
| 121 |
+
interactive=False
|
| 122 |
+
)
|
| 123 |
+
img_all = ImageMeta(
|
| 124 |
+
label="Upload Image (All metadata)",
|
| 125 |
+
only_custom_metadata=False,
|
| 126 |
+
type="filepath",
|
| 127 |
+
width=300,
|
| 128 |
+
height=400,
|
| 129 |
+
popup_metadata_height=400,
|
| 130 |
+
popup_metadata_width=500,
|
| 131 |
+
interactive=False
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
gr.Markdown("## Metadata Viewer")
|
| 135 |
+
gr.Markdown("### Individual Components")
|
| 136 |
+
with gr.Row():
|
| 137 |
+
model_box = gr.Textbox(label="Model")
|
| 138 |
+
fnumber_box = gr.Textbox(label="FNumber")
|
| 139 |
+
iso_box = gr.Textbox(label="ISOSpeedRatings")
|
| 140 |
+
s_churn = gr.Slider(label="Schurn", value=1.0, minimum=0.0, maximum=1.0, step=0.1)
|
| 141 |
+
description_box = gr.Textbox(label="Description")
|
| 142 |
+
|
| 143 |
+
gr.Markdown("### PropertySheet Component")
|
| 144 |
+
with gr.Row():
|
| 145 |
+
property_sheet = PropertySheet(
|
| 146 |
+
value=initial_property_from_meta_config,
|
| 147 |
+
label="Image Settings",
|
| 148 |
+
width=400,
|
| 149 |
+
height=550,
|
| 150 |
+
visible=True,
|
| 151 |
+
root_label="General"
|
| 152 |
+
)
|
| 153 |
+
gr.Markdown("## Metadata Editor")
|
| 154 |
+
with gr.Row():
|
| 155 |
+
save_button = gr.Button("Add Metadata and Save Image")
|
| 156 |
+
saved_file_output = gr.File(label="Download Image")
|
| 157 |
+
|
| 158 |
+
with gr.Row():
|
| 159 |
+
gr.Examples(
|
| 160 |
+
examples=[
|
| 161 |
+
["./examples/image_with_meta.png", "./examples/image_with_meta.png"]
|
| 162 |
+
],
|
| 163 |
+
fn=process_example_images,
|
| 164 |
+
inputs=[img_custom, img_all],
|
| 165 |
+
outputs=[img_custom, img_all],
|
| 166 |
+
cache_examples=True
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
input_fields = {
|
| 170 |
+
"Model": model_box,
|
| 171 |
+
"FNumber": fnumber_box,
|
| 172 |
+
"ISOSpeedRatings": iso_box,
|
| 173 |
+
"Schurn": s_churn,
|
| 174 |
+
"Description": description_box
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
output_fields = [
|
| 178 |
+
property_sheet,
|
| 179 |
+
model_box,
|
| 180 |
+
fnumber_box,
|
| 181 |
+
iso_box,
|
| 182 |
+
s_churn,
|
| 183 |
+
description_box
|
| 184 |
+
]
|
| 185 |
+
|
| 186 |
+
img_custom.load_metadata(handle_load_metadata, inputs=img_custom, outputs=output_fields)
|
| 187 |
+
img_all.load_metadata(handle_load_metadata, inputs=img_all, outputs=output_fields)
|
| 188 |
+
|
| 189 |
+
def handle_render_change(updated_config: PropertyConfig, current_state: PropertyConfig):
|
| 190 |
+
"""
|
| 191 |
+
Updates the PropertySheet state when its configuration changes.
|
| 192 |
+
|
| 193 |
+
Args:
|
| 194 |
+
updated_config: The new PropertyConfig instance from the PropertySheet.
|
| 195 |
+
current_state: The current PropertyConfig state.
|
| 196 |
+
|
| 197 |
+
Returns:
|
| 198 |
+
A tuple of (updated_config, updated_config) or (current_state, current_state) if updated_config is None.
|
| 199 |
+
"""
|
| 200 |
+
if updated_config is None:
|
| 201 |
+
return current_state, current_state
|
| 202 |
+
return updated_config, updated_config
|
| 203 |
+
|
| 204 |
+
property_sheet.change(
|
| 205 |
+
fn=handle_render_change,
|
| 206 |
+
inputs=[property_sheet, property_sheet_state],
|
| 207 |
+
outputs=[property_sheet, property_sheet_state]
|
| 208 |
+
)
|
| 209 |
+
save_button.click(
|
| 210 |
+
save_image_with_metadata,
|
| 211 |
+
inputs=[img_custom, *input_fields.values()],
|
| 212 |
+
outputs=[saved_file_output]
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
if __name__ == "__main__":
|
| 216 |
+
demo.launch()
|
css.css
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
html {
|
| 2 |
+
font-family: Inter;
|
| 3 |
+
font-size: 16px;
|
| 4 |
+
font-weight: 400;
|
| 5 |
+
line-height: 1.5;
|
| 6 |
+
-webkit-text-size-adjust: 100%;
|
| 7 |
+
background: #fff;
|
| 8 |
+
color: #323232;
|
| 9 |
+
-webkit-font-smoothing: antialiased;
|
| 10 |
+
-moz-osx-font-smoothing: grayscale;
|
| 11 |
+
text-rendering: optimizeLegibility;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
:root {
|
| 15 |
+
--space: 1;
|
| 16 |
+
--vspace: calc(var(--space) * 1rem);
|
| 17 |
+
--vspace-0: calc(3 * var(--space) * 1rem);
|
| 18 |
+
--vspace-1: calc(2 * var(--space) * 1rem);
|
| 19 |
+
--vspace-2: calc(1.5 * var(--space) * 1rem);
|
| 20 |
+
--vspace-3: calc(0.5 * var(--space) * 1rem);
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.app {
|
| 24 |
+
max-width: 748px !important;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.prose p {
|
| 28 |
+
margin: var(--vspace) 0;
|
| 29 |
+
line-height: var(--vspace * 2);
|
| 30 |
+
font-size: 1rem;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
code {
|
| 34 |
+
font-family: "Inconsolata", sans-serif;
|
| 35 |
+
font-size: 16px;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
h1,
|
| 39 |
+
h1 code {
|
| 40 |
+
font-weight: 400;
|
| 41 |
+
line-height: calc(2.5 / var(--space) * var(--vspace));
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
h1 code {
|
| 45 |
+
background: none;
|
| 46 |
+
border: none;
|
| 47 |
+
letter-spacing: 0.05em;
|
| 48 |
+
padding-bottom: 5px;
|
| 49 |
+
position: relative;
|
| 50 |
+
padding: 0;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
h2 {
|
| 54 |
+
margin: var(--vspace-1) 0 var(--vspace-2) 0;
|
| 55 |
+
line-height: 1em;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
h3,
|
| 59 |
+
h3 code {
|
| 60 |
+
margin: var(--vspace-1) 0 var(--vspace-2) 0;
|
| 61 |
+
line-height: 1em;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
h4,
|
| 65 |
+
h5,
|
| 66 |
+
h6 {
|
| 67 |
+
margin: var(--vspace-3) 0 var(--vspace-3) 0;
|
| 68 |
+
line-height: var(--vspace);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.bigtitle,
|
| 72 |
+
h1,
|
| 73 |
+
h1 code {
|
| 74 |
+
font-size: calc(8px * 4.5);
|
| 75 |
+
word-break: break-word;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.title,
|
| 79 |
+
h2,
|
| 80 |
+
h2 code {
|
| 81 |
+
font-size: calc(8px * 3.375);
|
| 82 |
+
font-weight: lighter;
|
| 83 |
+
word-break: break-word;
|
| 84 |
+
border: none;
|
| 85 |
+
background: none;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.subheading1,
|
| 89 |
+
h3,
|
| 90 |
+
h3 code {
|
| 91 |
+
font-size: calc(8px * 1.8);
|
| 92 |
+
font-weight: 600;
|
| 93 |
+
border: none;
|
| 94 |
+
background: none;
|
| 95 |
+
letter-spacing: 0.1em;
|
| 96 |
+
text-transform: uppercase;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
h2 code {
|
| 100 |
+
padding: 0;
|
| 101 |
+
position: relative;
|
| 102 |
+
letter-spacing: 0.05em;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
blockquote {
|
| 106 |
+
font-size: calc(8px * 1.1667);
|
| 107 |
+
font-style: italic;
|
| 108 |
+
line-height: calc(1.1667 * var(--vspace));
|
| 109 |
+
margin: var(--vspace-2) var(--vspace-2);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.subheading2,
|
| 113 |
+
h4 {
|
| 114 |
+
font-size: calc(8px * 1.4292);
|
| 115 |
+
text-transform: uppercase;
|
| 116 |
+
font-weight: 600;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.subheading3,
|
| 120 |
+
h5 {
|
| 121 |
+
font-size: calc(8px * 1.2917);
|
| 122 |
+
line-height: calc(1.2917 * var(--vspace));
|
| 123 |
+
|
| 124 |
+
font-weight: lighter;
|
| 125 |
+
text-transform: uppercase;
|
| 126 |
+
letter-spacing: 0.15em;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
h6 {
|
| 130 |
+
font-size: calc(8px * 1.1667);
|
| 131 |
+
font-size: 1.1667em;
|
| 132 |
+
font-weight: normal;
|
| 133 |
+
font-style: italic;
|
| 134 |
+
font-family: "le-monde-livre-classic-byol", serif !important;
|
| 135 |
+
letter-spacing: 0px !important;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
#start .md > *:first-child {
|
| 139 |
+
margin-top: 0;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
h2 + h3 {
|
| 143 |
+
margin-top: 0;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.md hr {
|
| 147 |
+
border: none;
|
| 148 |
+
border-top: 1px solid var(--block-border-color);
|
| 149 |
+
margin: var(--vspace-2) 0 var(--vspace-2) 0;
|
| 150 |
+
}
|
| 151 |
+
.prose ul {
|
| 152 |
+
margin: var(--vspace-2) 0 var(--vspace-1) 0;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.gap {
|
| 156 |
+
gap: 0;
|
| 157 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio_imagemeta
|
| 2 |
+
gradio_propertysheet
|
space.py
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import gradio as gr
|
| 3 |
+
from app import demo as app
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
_docs = {'ImageMeta': {'description': "A Gradio component for uploading or displaying images, with support for metadata extraction and a custom load_metadata event.\n\nThis component allows users to upload images (as input) or display images (as output). It includes a custom event for loading metadata, triggered by a 'Load Metadata' button in the UI, which expects ImageMetaData as input.", 'members': {'__init__': {'value': {'type': 'str | Image.Image | np.ndarray | Callable | None', 'default': 'None', 'description': 'A PIL Image, numpy array, path or URL for the default value that Image component is going to take. If a function is provided, the function will be called each time the app loads to set the initial value of this component.'}, 'format': {'type': 'str', 'default': '"webp"', 'description': 'File format (e.g. "png" or "gif"). Used to save image if it does not already have a valid format (e.g. if the image is being returned to the frontend as a numpy array or PIL Image). The format should be supported by the PIL library. Applies both when this component is used as an input or output. This parameter has no effect on SVG files.'}, 'height': {'type': 'int | str | None', 'default': 'None', 'description': 'The height of the component, specified in pixels if a number is passed, or in CSS units if a string is passed. This has no effect on the preprocessed image file or numpy array, but will affect the displayed image.'}, 'width': {'type': 'int | str | None', 'default': 'None', 'description': 'The width of the component, specified in pixels if a number is passed, or in CSS units if a string is passed. This has no effect on the preprocessed image file or numpy array, but will affect the displayed image.'}, 'image_mode': {'type': 'Literal[\n "1",\n "L",\n "P",\n "RGB",\n "RGBA",\n "CMYK",\n "YCbCr",\n "LAB",\n "HSV",\n "I",\n "F",\n ]\n | None', 'default': '"RGB"', 'description': 'The pixel format and color depth that the image should be loaded and preprocessed as. "RGB" will load the image as a color image, or "L" as black-and-white. See https://pillow.readthedocs.io/en/stable/handbook/concepts.html for other supported image modes and their meaning. This parameter has no effect on SVG or GIF files. If set to None, the image_mode will be inferred from the image file type (e.g. "RGBA" for a .png image, "RGB" in most other cases).'}, 'type': {'type': 'Literal["numpy", "pil", "filepath"]', 'default': '"numpy"', 'description': 'The format the image is converted before being passed into the prediction function. "numpy" converts the image to a numpy array with shape (height, width, 3) and values from 0 to 255, "pil" converts the image to a PIL image object, "filepath" passes a str path to a temporary file containing the image. To support animated GIFs in input, the `type` should be set to "filepath" or "pil". To support SVGs, the `type` should be set to "filepath".'}, 'label': {'type': 'str | I18nData | None', 'default': 'None', 'description': 'The label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.'}, 'every': {'type': 'Timer | float | None', 'default': 'None', 'description': 'Continously calls `value` to recalculate it if `value` is a function (has no effect otherwise). Can provide a Timer whose tick resets `value`, or a float that provides the regular interval for the reset Timer.'}, 'inputs': {'type': 'Component | Sequence[Component] | set[Component] | None', 'default': 'None', 'description': 'Components that are used as inputs to calculate `value` if `value` is a function (has no effect otherwise). `value` is recalculated any time the inputs change.'}, 'show_label': {'type': 'bool | None', 'default': 'None', 'description': 'If True, will display label.'}, 'show_download_button': {'type': 'bool', 'default': 'True', 'description': 'If True, will display button to download image. Only applies if interactive is False (e.g. if the component is used as an output).'}, 'container': {'type': 'bool', 'default': 'True', 'description': 'If True, will place the component in a container - providing some extra padding around the border.'}, 'scale': {'type': 'int | None', 'default': 'None', 'description': 'Relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.'}, 'min_width': {'type': 'int', 'default': '160', 'description': 'Minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.'}, 'interactive': {'type': 'bool | None', 'default': 'None', 'description': 'If True, will allow users to upload and edit an image; if False, can only be used to display images. If not provided, this is inferred based on whether the component is used as an input or output.'}, 'visible': {'type': 'bool', 'default': 'True', 'description': 'If False, component will be hidden.'}, 'elem_id': {'type': 'str | None', 'default': 'None', 'description': 'An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.'}, 'elem_classes': {'type': 'list[str] | str | None', 'default': 'None', 'description': 'An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.'}, 'render': {'type': 'bool', 'default': 'True', 'description': 'If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.'}, 'key': {'type': 'int | str | tuple[int | str, ...] | None', 'default': 'None', 'description': "In a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render."}, 'preserved_by_key': {'type': 'list[str] | str | None', 'default': '"value"', 'description': "A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor."}, 'show_share_button': {'type': 'bool | None', 'default': 'None', 'description': 'If True, will show a share icon in the corner of the component that allows user to share outputs to Hugging Face Spaces Discussions. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.'}, 'placeholder': {'type': 'str | None', 'default': 'None', 'description': 'Custom text for the upload area. Overrides default upload messages when provided. Accepts new lines and `#` to designate a heading.'}, 'show_fullscreen_button': {'type': 'bool', 'default': 'True', 'description': 'If True, will show a fullscreen icon in the corner of the component that allows user to view the image in fullscreen mode. If False, icon does not appear.'}, 'only_custom_metadata': {'type': 'bool', 'default': 'True', 'description': 'If True, extracts only custom metadata, excluding technical metadata like ImageWidth or ImageHeight. Defaults to True.'}, 'disable_preprocess': {'type': 'bool', 'default': 'True', 'description': 'If True, skips preprocessing and returns the raw ImageMetaData payload. Defaults to True.'}, 'popup_metadata_width': {'type': 'int | str', 'default': '400', 'description': 'Metadata popup width in pixels or CSS units. Defaults to 400.'}, 'popup_metadata_height': {'type': 'int | str', 'default': '300', 'description': 'Metadata popup height in pixels or CSS units. Defaults to 300.'}}, 'postprocess': {'value': {'type': 'numpy.ndarray | PIL.Image.Image | str | pathlib.Path | None', 'description': 'Input image as a NumPy array, PIL Image, string (file path or URL), Path object, or None.'}}, 'preprocess': {'return': {'type': 'numpy.ndarray | PIL.Image.Image | str | ImageMetaData | None', 'description': 'Preprocessed image as a NumPy array, PIL Image, filepath, ImageMetaData, or None.'}, 'value': None}}, 'events': {'clear': {'type': None, 'default': None, 'description': 'This listener is triggered when the user clears the ImageMeta using the clear button for the component.'}, 'change': {'type': None, 'default': None, 'description': 'Triggered when the value of the ImageMeta changes either because of user input (e.g. a user types in a textbox) OR because of a function update (e.g. an image receives a value from the output of an event trigger). See `.input()` for a listener that is only triggered by user input.'}, 'select': {'type': None, 'default': None, 'description': 'Event listener for when the user selects or deselects the ImageMeta. Uses event data gradio.SelectData to carry `value` referring to the label of the ImageMeta, and `selected` to refer to state of the ImageMeta. See EventData documentation on how to use this event data'}, 'upload': {'type': None, 'default': None, 'description': 'This listener is triggered when the user uploads a file into the ImageMeta.'}, 'input': {'type': None, 'default': None, 'description': 'This listener is triggered when the user changes the value of the ImageMeta.'}, 'load_metadata': {'type': None, 'default': None, 'description': "Triggered when the user clicks the 'Load Metadata' button, expecting ImageMetaData as input."}}}, '__meta__': {'additional_interfaces': {'ImageMetaData': {'source': 'class ImageMetaData(ImageData):\n pass'}}, 'user_fn_refs': {'ImageMeta': ['ImageMetaData']}}}
|
| 7 |
+
|
| 8 |
+
abs_path = os.path.join(os.path.dirname(__file__), "css.css")
|
| 9 |
+
|
| 10 |
+
with gr.Blocks(
|
| 11 |
+
css=abs_path,
|
| 12 |
+
theme=gr.themes.Default(
|
| 13 |
+
font_mono=[
|
| 14 |
+
gr.themes.GoogleFont("Inconsolata"),
|
| 15 |
+
"monospace",
|
| 16 |
+
],
|
| 17 |
+
),
|
| 18 |
+
) as demo:
|
| 19 |
+
gr.Markdown(
|
| 20 |
+
"""
|
| 21 |
+
# `gradio_imagemeta`
|
| 22 |
+
|
| 23 |
+
<div style="display: flex; gap: 7px;">
|
| 24 |
+
<img alt="Static Badge" src="https://img.shields.io/badge/version%20-%200.0.1%20-%20orange">
|
| 25 |
+
</div>
|
| 26 |
+
|
| 27 |
+
Image Preview with Metadata for Gradio Interface
|
| 28 |
+
""", elem_classes=["md-custom"], header_links=True)
|
| 29 |
+
app.render()
|
| 30 |
+
gr.Markdown(
|
| 31 |
+
"""
|
| 32 |
+
## Installation
|
| 33 |
+
|
| 34 |
+
```bash
|
| 35 |
+
pip install gradio_imagemeta
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
## Usage
|
| 39 |
+
|
| 40 |
+
```python
|
| 41 |
+
from dataclasses import dataclass, field
|
| 42 |
+
from typing import List, Any
|
| 43 |
+
import gradio as gr
|
| 44 |
+
from gradio_imagemeta import ImageMeta
|
| 45 |
+
from gradio_imagemeta.helpers import extract_metadata, add_metadata, transfer_metadata
|
| 46 |
+
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 |
+
|
| 53 |
+
@dataclass
|
| 54 |
+
class ImageSettings:
|
| 55 |
+
\"\"\"Configuration for image metadata settings.\"\"\"
|
| 56 |
+
model: str = field(default="", metadata={"label": "Model"})
|
| 57 |
+
f_number: str = field(default="", metadata={"label": "FNumber"})
|
| 58 |
+
iso_speed_ratings: str = field(default="", metadata={"label": "ISOSpeedRatings"})
|
| 59 |
+
s_churn: float = field(
|
| 60 |
+
default=0.0,
|
| 61 |
+
metadata={"component": "slider", "label": "Schurn", "minimum": 0.0, "maximum": 1.0, "step": 0.01},
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
@dataclass
|
| 65 |
+
class PropertyConfig:
|
| 66 |
+
\"\"\"Root configuration for image properties, including nested image settings.\"\"\"
|
| 67 |
+
image_settings: ImageSettings = field(default_factory=ImageSettings)
|
| 68 |
+
description: str = field(default="", metadata={"label": "Description"})
|
| 69 |
+
|
| 70 |
+
def process_example_images(img_custom_path: str, img_all_path: str) -> tuple[str, str]:
|
| 71 |
+
\"\"\"
|
| 72 |
+
Processes example image paths for display in ImageMeta components.
|
| 73 |
+
|
| 74 |
+
Args:
|
| 75 |
+
img_custom_path: File path for the image to display in img_custom.
|
| 76 |
+
img_all_path: File path for the image to display in img_all.
|
| 77 |
+
|
| 78 |
+
Returns:
|
| 79 |
+
Tuple of file paths for img_custom and img_all outputs.
|
| 80 |
+
\"\"\"
|
| 81 |
+
# Verify file existence
|
| 82 |
+
if not Path(img_custom_path).is_file():
|
| 83 |
+
raise FileNotFoundError(f"Image not found: {img_custom_path}")
|
| 84 |
+
if not Path(img_all_path).is_file():
|
| 85 |
+
raise FileNotFoundError(f"Image not found: {img_all_path}")
|
| 86 |
+
|
| 87 |
+
# Return file paths as strings (ImageMeta accepts file paths as input)
|
| 88 |
+
return str(img_custom_path), str(img_all_path)
|
| 89 |
+
|
| 90 |
+
def handle_load_metadata(image_data: ImageMeta | None) -> List[Any]:
|
| 91 |
+
\"\"\"
|
| 92 |
+
Processes image metadata and maps it to output components.
|
| 93 |
+
|
| 94 |
+
Args:
|
| 95 |
+
image_data: ImageMeta object containing image data and metadata, or None.
|
| 96 |
+
|
| 97 |
+
Returns:
|
| 98 |
+
A list of values for output components (Textbox, Slider, or PropertySheet instances).
|
| 99 |
+
\"\"\"
|
| 100 |
+
if not image_data:
|
| 101 |
+
return [gr.Textbox(value="") for _ in output_fields]
|
| 102 |
+
|
| 103 |
+
metadata = extract_metadata(image_data, only_custom_metadata=True)
|
| 104 |
+
dataclass_fields = build_dataclass_fields(PropertyConfig)
|
| 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.Textbox(value=value)
|
| 113 |
+
|
| 114 |
+
return output_values
|
| 115 |
+
|
| 116 |
+
def save_image_with_metadata(image_data: Any, *inputs: Any) -> str | None:
|
| 117 |
+
\"\"\"
|
| 118 |
+
Saves an image with updated metadata to a file.
|
| 119 |
+
|
| 120 |
+
Args:
|
| 121 |
+
image_data: Input image data (e.g., file path or PIL Image).
|
| 122 |
+
*inputs: Variable number of input values from UI components (Textbox, Slider).
|
| 123 |
+
|
| 124 |
+
Returns:
|
| 125 |
+
The file path of the saved image, or None if no image is provided.
|
| 126 |
+
\"\"\"
|
| 127 |
+
if not image_data:
|
| 128 |
+
return 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 dataclass_fields.keys()}
|
| 134 |
+
|
| 135 |
+
new_filepath = output_dir / "image_with_meta.png"
|
| 136 |
+
add_metadata(image_data, metadata, new_filepath)
|
| 137 |
+
|
| 138 |
+
return str(new_filepath)
|
| 139 |
+
|
| 140 |
+
initial_property_from_meta_config = PropertyConfig()
|
| 141 |
+
|
| 142 |
+
with gr.Blocks() as demo:
|
| 143 |
+
gr.Markdown("# ImageMeta Component Demo")
|
| 144 |
+
gr.Markdown(
|
| 145 |
+
\"\"\"
|
| 146 |
+
**To Test:**
|
| 147 |
+
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.
|
| 148 |
+
2. Click the 'Info' icon (ⓘ) in the top-left of the image component to view the metadata panel.
|
| 149 |
+
3. Click 'Load Metadata' in the popup to populate the fields below with metadata values (`Model`, `FNumber`, `ISOSpeedRatings`, `Schurn`, `Description`).
|
| 150 |
+
4. The section below displays how metadata is rendered in components and the `PropertySheet` custom component, showing the hierarchical structure of the image settings.
|
| 151 |
+
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.
|
| 152 |
+
\"\"\"
|
| 153 |
+
)
|
| 154 |
+
property_sheet_state = gr.State(value=initial_property_from_meta_config)
|
| 155 |
+
with gr.Row():
|
| 156 |
+
img_custom = ImageMeta(
|
| 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")
|
| 174 |
+
gr.Markdown("### Individual Components")
|
| 175 |
+
with gr.Row():
|
| 176 |
+
model_box = gr.Textbox(label="Model")
|
| 177 |
+
fnumber_box = gr.Textbox(label="FNumber")
|
| 178 |
+
iso_box = gr.Textbox(label="ISOSpeedRatings")
|
| 179 |
+
s_churn = gr.Slider(label="Schurn", value=1.0, minimum=0.0, maximum=1.0, step=0.1)
|
| 180 |
+
description_box = gr.Textbox(label="Description")
|
| 181 |
+
|
| 182 |
+
gr.Markdown("### PropertySheet Component")
|
| 183 |
+
with gr.Row():
|
| 184 |
+
property_sheet = PropertySheet(
|
| 185 |
+
value=initial_property_from_meta_config,
|
| 186 |
+
label="Image Settings",
|
| 187 |
+
width=400,
|
| 188 |
+
height=550,
|
| 189 |
+
visible=True,
|
| 190 |
+
root_label="General"
|
| 191 |
+
)
|
| 192 |
+
gr.Markdown("## Metadata Editor")
|
| 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,
|
| 210 |
+
"FNumber": fnumber_box,
|
| 211 |
+
"ISOSpeedRatings": iso_box,
|
| 212 |
+
"Schurn": s_churn,
|
| 213 |
+
"Description": description_box
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
output_fields = [
|
| 217 |
+
property_sheet,
|
| 218 |
+
model_box,
|
| 219 |
+
fnumber_box,
|
| 220 |
+
iso_box,
|
| 221 |
+
s_churn,
|
| 222 |
+
description_box
|
| 223 |
+
]
|
| 224 |
+
|
| 225 |
+
img_custom.load_metadata(handle_load_metadata, inputs=img_custom, outputs=output_fields)
|
| 226 |
+
img_all.load_metadata(handle_load_metadata, inputs=img_all, outputs=output_fields)
|
| 227 |
+
|
| 228 |
+
def handle_render_change(updated_config: PropertyConfig, current_state: PropertyConfig):
|
| 229 |
+
\"\"\"
|
| 230 |
+
Updates the PropertySheet state when its configuration changes.
|
| 231 |
+
|
| 232 |
+
Args:
|
| 233 |
+
updated_config: The new PropertyConfig instance from the PropertySheet.
|
| 234 |
+
current_state: The current PropertyConfig state.
|
| 235 |
+
|
| 236 |
+
Returns:
|
| 237 |
+
A tuple of (updated_config, updated_config) or (current_state, current_state) if updated_config is None.
|
| 238 |
+
\"\"\"
|
| 239 |
+
if updated_config is None:
|
| 240 |
+
return current_state, current_state
|
| 241 |
+
return updated_config, updated_config
|
| 242 |
+
|
| 243 |
+
property_sheet.change(
|
| 244 |
+
fn=handle_render_change,
|
| 245 |
+
inputs=[property_sheet, property_sheet_state],
|
| 246 |
+
outputs=[property_sheet, property_sheet_state]
|
| 247 |
+
)
|
| 248 |
+
save_button.click(
|
| 249 |
+
save_image_with_metadata,
|
| 250 |
+
inputs=[img_custom, *input_fields.values()],
|
| 251 |
+
outputs=[saved_file_output]
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
if __name__ == "__main__":
|
| 255 |
+
demo.launch()
|
| 256 |
+
```
|
| 257 |
+
""", elem_classes=["md-custom"], header_links=True)
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
gr.Markdown("""
|
| 261 |
+
## `ImageMeta`
|
| 262 |
+
|
| 263 |
+
### Initialization
|
| 264 |
+
""", elem_classes=["md-custom"], header_links=True)
|
| 265 |
+
|
| 266 |
+
gr.ParamViewer(value=_docs["ImageMeta"]["members"]["__init__"], linkify=['ImageMetaData'])
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
gr.Markdown("### Events")
|
| 270 |
+
gr.ParamViewer(value=_docs["ImageMeta"]["events"], linkify=['Event'])
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
gr.Markdown("""
|
| 276 |
+
|
| 277 |
+
### User function
|
| 278 |
+
|
| 279 |
+
The impact on the users predict function varies depending on whether the component is used as an input or output for an event (or both).
|
| 280 |
+
|
| 281 |
+
- When used as an Input, the component only impacts the input signature of the user function.
|
| 282 |
+
- When used as an output, the component only impacts the return signature of the user function.
|
| 283 |
+
|
| 284 |
+
The code snippet below is accurate in cases where the component is used as both an input and an output.
|
| 285 |
+
|
| 286 |
+
- **As input:** Is passed, preprocessed image as a NumPy array, PIL Image, filepath, ImageMetaData, or None.
|
| 287 |
+
- **As output:** Should return, input image as a NumPy array, PIL Image, string (file path or URL), Path object, or None.
|
| 288 |
+
|
| 289 |
+
```python
|
| 290 |
+
def predict(
|
| 291 |
+
value: numpy.ndarray | PIL.Image.Image | str | ImageMetaData | None
|
| 292 |
+
) -> numpy.ndarray | PIL.Image.Image | str | pathlib.Path | None:
|
| 293 |
+
return value
|
| 294 |
+
```
|
| 295 |
+
""", elem_classes=["md-custom", "ImageMeta-user-fn"], header_links=True)
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
code_ImageMetaData = gr.Markdown("""
|
| 301 |
+
## `ImageMetaData`
|
| 302 |
+
```python
|
| 303 |
+
class ImageMetaData(ImageData):
|
| 304 |
+
pass
|
| 305 |
+
```""", elem_classes=["md-custom", "ImageMetaData"], header_links=True)
|
| 306 |
+
|
| 307 |
+
demo.load(None, js=r"""function() {
|
| 308 |
+
const refs = {
|
| 309 |
+
ImageMetaData: [], };
|
| 310 |
+
const user_fn_refs = {
|
| 311 |
+
ImageMeta: ['ImageMetaData'], };
|
| 312 |
+
requestAnimationFrame(() => {
|
| 313 |
+
|
| 314 |
+
Object.entries(user_fn_refs).forEach(([key, refs]) => {
|
| 315 |
+
if (refs.length > 0) {
|
| 316 |
+
const el = document.querySelector(`.${key}-user-fn`);
|
| 317 |
+
if (!el) return;
|
| 318 |
+
refs.forEach(ref => {
|
| 319 |
+
el.innerHTML = el.innerHTML.replace(
|
| 320 |
+
new RegExp("\\b"+ref+"\\b", "g"),
|
| 321 |
+
`<a href="#h-${ref.toLowerCase()}">${ref}</a>`
|
| 322 |
+
);
|
| 323 |
+
})
|
| 324 |
+
}
|
| 325 |
+
})
|
| 326 |
+
|
| 327 |
+
Object.entries(refs).forEach(([key, refs]) => {
|
| 328 |
+
if (refs.length > 0) {
|
| 329 |
+
const el = document.querySelector(`.${key}`);
|
| 330 |
+
if (!el) return;
|
| 331 |
+
refs.forEach(ref => {
|
| 332 |
+
el.innerHTML = el.innerHTML.replace(
|
| 333 |
+
new RegExp("\\b"+ref+"\\b", "g"),
|
| 334 |
+
`<a href="#h-${ref.toLowerCase()}">${ref}</a>`
|
| 335 |
+
);
|
| 336 |
+
})
|
| 337 |
+
}
|
| 338 |
+
})
|
| 339 |
+
})
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
""")
|
| 343 |
+
|
| 344 |
+
demo.launch()
|
src/.gitignore
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.eggs/
|
| 2 |
+
.vscode/
|
| 3 |
+
.gradio/
|
| 4 |
+
dist/
|
| 5 |
+
*.pyc
|
| 6 |
+
__pycache__/
|
| 7 |
+
*.py[cod]
|
| 8 |
+
*$py.class
|
| 9 |
+
__tmp/*
|
| 10 |
+
*.pyi
|
| 11 |
+
.mypycache
|
| 12 |
+
.ruff_cache
|
| 13 |
+
node_modules
|
| 14 |
+
backend/**/templates/
|
| 15 |
+
outputs/
|
src/.gradio/cached_examples/24/Upload Image All metadata/7a159874ef63b94d8fa4/image_with_meta.png
ADDED
|
Git LFS Details
|
src/.gradio/cached_examples/24/Upload Image Custom metadata only/8a0de8f0e921a79b67a8/image_with_meta.png
ADDED
|
Git LFS Details
|
src/.gradio/cached_examples/24/indices.csv
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
0
|
src/.gradio/cached_examples/24/log.csv
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Upload Image (Custom metadata only),Upload Image (All metadata),timestamp
|
| 2 |
+
"{""path"": "".gradio\\cached_examples\\24\\Upload Image Custom metadata only\\8a0de8f0e921a79b67a8\\image_with_meta.png"", ""url"": ""/gradio_api/file=C:\\Users\\pelol\\AppData\\Local\\Temp\\gradio\\8712b2e1fb048f8ef5b4cc59f8a9aa7d34a5f09f2ae3384b5b3b4afe6c5bc52a\\image_with_meta.png"", ""size"": null, ""orig_name"": ""image_with_meta.png"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}","{""path"": "".gradio\\cached_examples\\24\\Upload Image All metadata\\7a159874ef63b94d8fa4\\image_with_meta.png"", ""url"": ""/gradio_api/file=C:\\Users\\pelol\\AppData\\Local\\Temp\\gradio\\8712b2e1fb048f8ef5b4cc59f8a9aa7d34a5f09f2ae3384b5b3b4afe6c5bc52a\\image_with_meta.png"", ""size"": null, ""orig_name"": ""image_with_meta.png"", ""mime_type"": null, ""is_stream"": false, ""meta"": {""_type"": ""gradio.FileData""}}",2025-08-11 19:51:20.821323
|
src/.vscode/launch.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
// Use IntelliSense to learn about possible attributes.
|
| 3 |
+
// Hover to view descriptions of existing attributes.
|
| 4 |
+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
| 5 |
+
"version": "0.2.0",
|
| 6 |
+
"configurations": [
|
| 7 |
+
{
|
| 8 |
+
"name": "Python Debugger: Current File",
|
| 9 |
+
"type": "debugpy",
|
| 10 |
+
"request": "launch",
|
| 11 |
+
"program": "${file}",
|
| 12 |
+
"console": "integratedTerminal",
|
| 13 |
+
"justMyCode": false
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
"name": "Gradio dev (Python attach)",
|
| 17 |
+
"type": "debugpy",
|
| 18 |
+
"request": "attach",
|
| 19 |
+
"processId": "${command:pickProcess}",
|
| 20 |
+
"justMyCode": false
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"name": "Gradio dev (Svelte attach)",
|
| 24 |
+
"type": "chrome",
|
| 25 |
+
"request": "attach",
|
| 26 |
+
"port": 9222,
|
| 27 |
+
}
|
| 28 |
+
]
|
| 29 |
+
}
|
src/README.md
ADDED
|
@@ -0,0 +1,673 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
tags: [gradio-custom-component, Image]
|
| 3 |
+
title: gradio_imagemeta
|
| 4 |
+
short_description: Image Preview with Metadata for Gradio Interface
|
| 5 |
+
colorFrom: blue
|
| 6 |
+
colorTo: yellow
|
| 7 |
+
sdk: gradio
|
| 8 |
+
pinned: false
|
| 9 |
+
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-%20orange">
|
| 14 |
+
|
| 15 |
+
Image Preview with Metadata for Gradio Interface
|
| 16 |
+
|
| 17 |
+
## Installation
|
| 18 |
+
|
| 19 |
+
```bash
|
| 20 |
+
pip install gradio_imagemeta
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
## Usage
|
| 24 |
+
|
| 25 |
+
```python
|
| 26 |
+
from dataclasses import dataclass, field
|
| 27 |
+
from typing import List, Any
|
| 28 |
+
import gradio as gr
|
| 29 |
+
from gradio_imagemeta import ImageMeta
|
| 30 |
+
from gradio_imagemeta.helpers import extract_metadata, add_metadata, transfer_metadata
|
| 31 |
+
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 |
+
|
| 38 |
+
@dataclass
|
| 39 |
+
class ImageSettings:
|
| 40 |
+
"""Configuration for image metadata settings."""
|
| 41 |
+
model: str = field(default="", metadata={"label": "Model"})
|
| 42 |
+
f_number: str = field(default="", metadata={"label": "FNumber"})
|
| 43 |
+
iso_speed_ratings: str = field(default="", metadata={"label": "ISOSpeedRatings"})
|
| 44 |
+
s_churn: float = field(
|
| 45 |
+
default=0.0,
|
| 46 |
+
metadata={"component": "slider", "label": "Schurn", "minimum": 0.0, "maximum": 1.0, "step": 0.01},
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
@dataclass
|
| 50 |
+
class PropertyConfig:
|
| 51 |
+
"""Root configuration for image properties, including nested image settings."""
|
| 52 |
+
image_settings: ImageSettings = field(default_factory=ImageSettings)
|
| 53 |
+
description: str = field(default="", metadata={"label": "Description"})
|
| 54 |
+
|
| 55 |
+
def process_example_images(img_custom_path: str, img_all_path: str) -> tuple[str, str]:
|
| 56 |
+
"""
|
| 57 |
+
Processes example image paths for display in ImageMeta components.
|
| 58 |
+
|
| 59 |
+
Args:
|
| 60 |
+
img_custom_path: File path for the image to display in img_custom.
|
| 61 |
+
img_all_path: File path for the image to display in img_all.
|
| 62 |
+
|
| 63 |
+
Returns:
|
| 64 |
+
Tuple of file paths for img_custom and img_all outputs.
|
| 65 |
+
"""
|
| 66 |
+
# Verify file existence
|
| 67 |
+
if not Path(img_custom_path).is_file():
|
| 68 |
+
raise FileNotFoundError(f"Image not found: {img_custom_path}")
|
| 69 |
+
if not Path(img_all_path).is_file():
|
| 70 |
+
raise FileNotFoundError(f"Image not found: {img_all_path}")
|
| 71 |
+
|
| 72 |
+
# Return file paths as strings (ImageMeta accepts file paths as input)
|
| 73 |
+
return str(img_custom_path), str(img_all_path)
|
| 74 |
+
|
| 75 |
+
def handle_load_metadata(image_data: ImageMeta | None) -> List[Any]:
|
| 76 |
+
"""
|
| 77 |
+
Processes image metadata and maps it to output components.
|
| 78 |
+
|
| 79 |
+
Args:
|
| 80 |
+
image_data: ImageMeta object containing image data and metadata, or None.
|
| 81 |
+
|
| 82 |
+
Returns:
|
| 83 |
+
A list of values for output components (Textbox, Slider, or PropertySheet instances).
|
| 84 |
+
"""
|
| 85 |
+
if not image_data:
|
| 86 |
+
return [gr.Textbox(value="") for _ in output_fields]
|
| 87 |
+
|
| 88 |
+
metadata = extract_metadata(image_data, only_custom_metadata=True)
|
| 89 |
+
dataclass_fields = build_dataclass_fields(PropertyConfig)
|
| 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.Textbox(value=value)
|
| 98 |
+
|
| 99 |
+
return output_values
|
| 100 |
+
|
| 101 |
+
def save_image_with_metadata(image_data: Any, *inputs: Any) -> str | None:
|
| 102 |
+
"""
|
| 103 |
+
Saves an image with updated metadata to a file.
|
| 104 |
+
|
| 105 |
+
Args:
|
| 106 |
+
image_data: Input image data (e.g., file path or PIL Image).
|
| 107 |
+
*inputs: Variable number of input values from UI components (Textbox, Slider).
|
| 108 |
+
|
| 109 |
+
Returns:
|
| 110 |
+
The file path of the saved image, or None if no image is provided.
|
| 111 |
+
"""
|
| 112 |
+
if not image_data:
|
| 113 |
+
return 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 dataclass_fields.keys()}
|
| 119 |
+
|
| 120 |
+
new_filepath = output_dir / "image_with_meta.png"
|
| 121 |
+
add_metadata(image_data, metadata, new_filepath)
|
| 122 |
+
|
| 123 |
+
return str(new_filepath)
|
| 124 |
+
|
| 125 |
+
initial_property_from_meta_config = PropertyConfig()
|
| 126 |
+
|
| 127 |
+
with gr.Blocks() as demo:
|
| 128 |
+
gr.Markdown("# ImageMeta Component Demo")
|
| 129 |
+
gr.Markdown(
|
| 130 |
+
"""
|
| 131 |
+
**To Test:**
|
| 132 |
+
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.
|
| 133 |
+
2. Click the 'Info' icon (ⓘ) in the top-left of the image component to view the metadata panel.
|
| 134 |
+
3. Click 'Load Metadata' in the popup to populate the fields below with metadata values (`Model`, `FNumber`, `ISOSpeedRatings`, `Schurn`, `Description`).
|
| 135 |
+
4. The section below displays how metadata is rendered in components and the `PropertySheet` custom component, showing the hierarchical structure of the image settings.
|
| 136 |
+
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.
|
| 137 |
+
"""
|
| 138 |
+
)
|
| 139 |
+
property_sheet_state = gr.State(value=initial_property_from_meta_config)
|
| 140 |
+
with gr.Row():
|
| 141 |
+
img_custom = ImageMeta(
|
| 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")
|
| 159 |
+
gr.Markdown("### Individual Components")
|
| 160 |
+
with gr.Row():
|
| 161 |
+
model_box = gr.Textbox(label="Model")
|
| 162 |
+
fnumber_box = gr.Textbox(label="FNumber")
|
| 163 |
+
iso_box = gr.Textbox(label="ISOSpeedRatings")
|
| 164 |
+
s_churn = gr.Slider(label="Schurn", value=1.0, minimum=0.0, maximum=1.0, step=0.1)
|
| 165 |
+
description_box = gr.Textbox(label="Description")
|
| 166 |
+
|
| 167 |
+
gr.Markdown("### PropertySheet Component")
|
| 168 |
+
with gr.Row():
|
| 169 |
+
property_sheet = PropertySheet(
|
| 170 |
+
value=initial_property_from_meta_config,
|
| 171 |
+
label="Image Settings",
|
| 172 |
+
width=400,
|
| 173 |
+
height=550,
|
| 174 |
+
visible=True,
|
| 175 |
+
root_label="General"
|
| 176 |
+
)
|
| 177 |
+
gr.Markdown("## Metadata Editor")
|
| 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,
|
| 195 |
+
"FNumber": fnumber_box,
|
| 196 |
+
"ISOSpeedRatings": iso_box,
|
| 197 |
+
"Schurn": s_churn,
|
| 198 |
+
"Description": description_box
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
output_fields = [
|
| 202 |
+
property_sheet,
|
| 203 |
+
model_box,
|
| 204 |
+
fnumber_box,
|
| 205 |
+
iso_box,
|
| 206 |
+
s_churn,
|
| 207 |
+
description_box
|
| 208 |
+
]
|
| 209 |
+
|
| 210 |
+
img_custom.load_metadata(handle_load_metadata, inputs=img_custom, outputs=output_fields)
|
| 211 |
+
img_all.load_metadata(handle_load_metadata, inputs=img_all, outputs=output_fields)
|
| 212 |
+
|
| 213 |
+
def handle_render_change(updated_config: PropertyConfig, current_state: PropertyConfig):
|
| 214 |
+
"""
|
| 215 |
+
Updates the PropertySheet state when its configuration changes.
|
| 216 |
+
|
| 217 |
+
Args:
|
| 218 |
+
updated_config: The new PropertyConfig instance from the PropertySheet.
|
| 219 |
+
current_state: The current PropertyConfig state.
|
| 220 |
+
|
| 221 |
+
Returns:
|
| 222 |
+
A tuple of (updated_config, updated_config) or (current_state, current_state) if updated_config is None.
|
| 223 |
+
"""
|
| 224 |
+
if updated_config is None:
|
| 225 |
+
return current_state, current_state
|
| 226 |
+
return updated_config, updated_config
|
| 227 |
+
|
| 228 |
+
property_sheet.change(
|
| 229 |
+
fn=handle_render_change,
|
| 230 |
+
inputs=[property_sheet, property_sheet_state],
|
| 231 |
+
outputs=[property_sheet, property_sheet_state]
|
| 232 |
+
)
|
| 233 |
+
save_button.click(
|
| 234 |
+
save_image_with_metadata,
|
| 235 |
+
inputs=[img_custom, *input_fields.values()],
|
| 236 |
+
outputs=[saved_file_output]
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
if __name__ == "__main__":
|
| 240 |
+
demo.launch()
|
| 241 |
+
```
|
| 242 |
+
|
| 243 |
+
## `ImageMeta`
|
| 244 |
+
|
| 245 |
+
### Initialization
|
| 246 |
+
|
| 247 |
+
<table>
|
| 248 |
+
<thead>
|
| 249 |
+
<tr>
|
| 250 |
+
<th align="left">name</th>
|
| 251 |
+
<th align="left" style="width: 25%;">type</th>
|
| 252 |
+
<th align="left">default</th>
|
| 253 |
+
<th align="left">description</th>
|
| 254 |
+
</tr>
|
| 255 |
+
</thead>
|
| 256 |
+
<tbody>
|
| 257 |
+
<tr>
|
| 258 |
+
<td align="left"><code>value</code></td>
|
| 259 |
+
<td align="left" style="width: 25%;">
|
| 260 |
+
|
| 261 |
+
```python
|
| 262 |
+
str | Image.Image | np.ndarray | Callable | None
|
| 263 |
+
```
|
| 264 |
+
|
| 265 |
+
</td>
|
| 266 |
+
<td align="left"><code>None</code></td>
|
| 267 |
+
<td align="left">A PIL Image, numpy array, path or URL for the default value that Image component is going to take. If a function is provided, the function will be called each time the app loads to set the initial value of this component.</td>
|
| 268 |
+
</tr>
|
| 269 |
+
|
| 270 |
+
<tr>
|
| 271 |
+
<td align="left"><code>format</code></td>
|
| 272 |
+
<td align="left" style="width: 25%;">
|
| 273 |
+
|
| 274 |
+
```python
|
| 275 |
+
str
|
| 276 |
+
```
|
| 277 |
+
|
| 278 |
+
</td>
|
| 279 |
+
<td align="left"><code>"webp"</code></td>
|
| 280 |
+
<td align="left">File format (e.g. "png" or "gif"). Used to save image if it does not already have a valid format (e.g. if the image is being returned to the frontend as a numpy array or PIL Image). The format should be supported by the PIL library. Applies both when this component is used as an input or output. This parameter has no effect on SVG files.</td>
|
| 281 |
+
</tr>
|
| 282 |
+
|
| 283 |
+
<tr>
|
| 284 |
+
<td align="left"><code>height</code></td>
|
| 285 |
+
<td align="left" style="width: 25%;">
|
| 286 |
+
|
| 287 |
+
```python
|
| 288 |
+
int | str | None
|
| 289 |
+
```
|
| 290 |
+
|
| 291 |
+
</td>
|
| 292 |
+
<td align="left"><code>None</code></td>
|
| 293 |
+
<td align="left">The height of the component, specified in pixels if a number is passed, or in CSS units if a string is passed. This has no effect on the preprocessed image file or numpy array, but will affect the displayed image.</td>
|
| 294 |
+
</tr>
|
| 295 |
+
|
| 296 |
+
<tr>
|
| 297 |
+
<td align="left"><code>width</code></td>
|
| 298 |
+
<td align="left" style="width: 25%;">
|
| 299 |
+
|
| 300 |
+
```python
|
| 301 |
+
int | str | None
|
| 302 |
+
```
|
| 303 |
+
|
| 304 |
+
</td>
|
| 305 |
+
<td align="left"><code>None</code></td>
|
| 306 |
+
<td align="left">The width of the component, specified in pixels if a number is passed, or in CSS units if a string is passed. This has no effect on the preprocessed image file or numpy array, but will affect the displayed image.</td>
|
| 307 |
+
</tr>
|
| 308 |
+
|
| 309 |
+
<tr>
|
| 310 |
+
<td align="left"><code>image_mode</code></td>
|
| 311 |
+
<td align="left" style="width: 25%;">
|
| 312 |
+
|
| 313 |
+
```python
|
| 314 |
+
Literal[
|
| 315 |
+
"1",
|
| 316 |
+
"L",
|
| 317 |
+
"P",
|
| 318 |
+
"RGB",
|
| 319 |
+
"RGBA",
|
| 320 |
+
"CMYK",
|
| 321 |
+
"YCbCr",
|
| 322 |
+
"LAB",
|
| 323 |
+
"HSV",
|
| 324 |
+
"I",
|
| 325 |
+
"F",
|
| 326 |
+
]
|
| 327 |
+
| None
|
| 328 |
+
```
|
| 329 |
+
|
| 330 |
+
</td>
|
| 331 |
+
<td align="left"><code>"RGB"</code></td>
|
| 332 |
+
<td align="left">The pixel format and color depth that the image should be loaded and preprocessed as. "RGB" will load the image as a color image, or "L" as black-and-white. See https://pillow.readthedocs.io/en/stable/handbook/concepts.html for other supported image modes and their meaning. This parameter has no effect on SVG or GIF files. If set to None, the image_mode will be inferred from the image file type (e.g. "RGBA" for a .png image, "RGB" in most other cases).</td>
|
| 333 |
+
</tr>
|
| 334 |
+
|
| 335 |
+
<tr>
|
| 336 |
+
<td align="left"><code>type</code></td>
|
| 337 |
+
<td align="left" style="width: 25%;">
|
| 338 |
+
|
| 339 |
+
```python
|
| 340 |
+
Literal["numpy", "pil", "filepath"]
|
| 341 |
+
```
|
| 342 |
+
|
| 343 |
+
</td>
|
| 344 |
+
<td align="left"><code>"numpy"</code></td>
|
| 345 |
+
<td align="left">The format the image is converted before being passed into the prediction function. "numpy" converts the image to a numpy array with shape (height, width, 3) and values from 0 to 255, "pil" converts the image to a PIL image object, "filepath" passes a str path to a temporary file containing the image. To support animated GIFs in input, the `type` should be set to "filepath" or "pil". To support SVGs, the `type` should be set to "filepath".</td>
|
| 346 |
+
</tr>
|
| 347 |
+
|
| 348 |
+
<tr>
|
| 349 |
+
<td align="left"><code>label</code></td>
|
| 350 |
+
<td align="left" style="width: 25%;">
|
| 351 |
+
|
| 352 |
+
```python
|
| 353 |
+
str | I18nData | None
|
| 354 |
+
```
|
| 355 |
+
|
| 356 |
+
</td>
|
| 357 |
+
<td align="left"><code>None</code></td>
|
| 358 |
+
<td align="left">The label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.</td>
|
| 359 |
+
</tr>
|
| 360 |
+
|
| 361 |
+
<tr>
|
| 362 |
+
<td align="left"><code>every</code></td>
|
| 363 |
+
<td align="left" style="width: 25%;">
|
| 364 |
+
|
| 365 |
+
```python
|
| 366 |
+
Timer | float | None
|
| 367 |
+
```
|
| 368 |
+
|
| 369 |
+
</td>
|
| 370 |
+
<td align="left"><code>None</code></td>
|
| 371 |
+
<td align="left">Continously calls `value` to recalculate it if `value` is a function (has no effect otherwise). Can provide a Timer whose tick resets `value`, or a float that provides the regular interval for the reset Timer.</td>
|
| 372 |
+
</tr>
|
| 373 |
+
|
| 374 |
+
<tr>
|
| 375 |
+
<td align="left"><code>inputs</code></td>
|
| 376 |
+
<td align="left" style="width: 25%;">
|
| 377 |
+
|
| 378 |
+
```python
|
| 379 |
+
Component | Sequence[Component] | set[Component] | None
|
| 380 |
+
```
|
| 381 |
+
|
| 382 |
+
</td>
|
| 383 |
+
<td align="left"><code>None</code></td>
|
| 384 |
+
<td align="left">Components that are used as inputs to calculate `value` if `value` is a function (has no effect otherwise). `value` is recalculated any time the inputs change.</td>
|
| 385 |
+
</tr>
|
| 386 |
+
|
| 387 |
+
<tr>
|
| 388 |
+
<td align="left"><code>show_label</code></td>
|
| 389 |
+
<td align="left" style="width: 25%;">
|
| 390 |
+
|
| 391 |
+
```python
|
| 392 |
+
bool | None
|
| 393 |
+
```
|
| 394 |
+
|
| 395 |
+
</td>
|
| 396 |
+
<td align="left"><code>None</code></td>
|
| 397 |
+
<td align="left">If True, will display label.</td>
|
| 398 |
+
</tr>
|
| 399 |
+
|
| 400 |
+
<tr>
|
| 401 |
+
<td align="left"><code>show_download_button</code></td>
|
| 402 |
+
<td align="left" style="width: 25%;">
|
| 403 |
+
|
| 404 |
+
```python
|
| 405 |
+
bool
|
| 406 |
+
```
|
| 407 |
+
|
| 408 |
+
</td>
|
| 409 |
+
<td align="left"><code>True</code></td>
|
| 410 |
+
<td align="left">If True, will display button to download image. Only applies if interactive is False (e.g. if the component is used as an output).</td>
|
| 411 |
+
</tr>
|
| 412 |
+
|
| 413 |
+
<tr>
|
| 414 |
+
<td align="left"><code>container</code></td>
|
| 415 |
+
<td align="left" style="width: 25%;">
|
| 416 |
+
|
| 417 |
+
```python
|
| 418 |
+
bool
|
| 419 |
+
```
|
| 420 |
+
|
| 421 |
+
</td>
|
| 422 |
+
<td align="left"><code>True</code></td>
|
| 423 |
+
<td align="left">If True, will place the component in a container - providing some extra padding around the border.</td>
|
| 424 |
+
</tr>
|
| 425 |
+
|
| 426 |
+
<tr>
|
| 427 |
+
<td align="left"><code>scale</code></td>
|
| 428 |
+
<td align="left" style="width: 25%;">
|
| 429 |
+
|
| 430 |
+
```python
|
| 431 |
+
int | None
|
| 432 |
+
```
|
| 433 |
+
|
| 434 |
+
</td>
|
| 435 |
+
<td align="left"><code>None</code></td>
|
| 436 |
+
<td align="left">Relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.</td>
|
| 437 |
+
</tr>
|
| 438 |
+
|
| 439 |
+
<tr>
|
| 440 |
+
<td align="left"><code>min_width</code></td>
|
| 441 |
+
<td align="left" style="width: 25%;">
|
| 442 |
+
|
| 443 |
+
```python
|
| 444 |
+
int
|
| 445 |
+
```
|
| 446 |
+
|
| 447 |
+
</td>
|
| 448 |
+
<td align="left"><code>160</code></td>
|
| 449 |
+
<td align="left">Minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.</td>
|
| 450 |
+
</tr>
|
| 451 |
+
|
| 452 |
+
<tr>
|
| 453 |
+
<td align="left"><code>interactive</code></td>
|
| 454 |
+
<td align="left" style="width: 25%;">
|
| 455 |
+
|
| 456 |
+
```python
|
| 457 |
+
bool | None
|
| 458 |
+
```
|
| 459 |
+
|
| 460 |
+
</td>
|
| 461 |
+
<td align="left"><code>None</code></td>
|
| 462 |
+
<td align="left">If True, will allow users to upload and edit an image; if False, can only be used to display images. If not provided, this is inferred based on whether the component is used as an input or output.</td>
|
| 463 |
+
</tr>
|
| 464 |
+
|
| 465 |
+
<tr>
|
| 466 |
+
<td align="left"><code>visible</code></td>
|
| 467 |
+
<td align="left" style="width: 25%;">
|
| 468 |
+
|
| 469 |
+
```python
|
| 470 |
+
bool
|
| 471 |
+
```
|
| 472 |
+
|
| 473 |
+
</td>
|
| 474 |
+
<td align="left"><code>True</code></td>
|
| 475 |
+
<td align="left">If False, component will be hidden.</td>
|
| 476 |
+
</tr>
|
| 477 |
+
|
| 478 |
+
<tr>
|
| 479 |
+
<td align="left"><code>elem_id</code></td>
|
| 480 |
+
<td align="left" style="width: 25%;">
|
| 481 |
+
|
| 482 |
+
```python
|
| 483 |
+
str | None
|
| 484 |
+
```
|
| 485 |
+
|
| 486 |
+
</td>
|
| 487 |
+
<td align="left"><code>None</code></td>
|
| 488 |
+
<td align="left">An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.</td>
|
| 489 |
+
</tr>
|
| 490 |
+
|
| 491 |
+
<tr>
|
| 492 |
+
<td align="left"><code>elem_classes</code></td>
|
| 493 |
+
<td align="left" style="width: 25%;">
|
| 494 |
+
|
| 495 |
+
```python
|
| 496 |
+
list[str] | str | None
|
| 497 |
+
```
|
| 498 |
+
|
| 499 |
+
</td>
|
| 500 |
+
<td align="left"><code>None</code></td>
|
| 501 |
+
<td align="left">An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.</td>
|
| 502 |
+
</tr>
|
| 503 |
+
|
| 504 |
+
<tr>
|
| 505 |
+
<td align="left"><code>render</code></td>
|
| 506 |
+
<td align="left" style="width: 25%;">
|
| 507 |
+
|
| 508 |
+
```python
|
| 509 |
+
bool
|
| 510 |
+
```
|
| 511 |
+
|
| 512 |
+
</td>
|
| 513 |
+
<td align="left"><code>True</code></td>
|
| 514 |
+
<td align="left">If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.</td>
|
| 515 |
+
</tr>
|
| 516 |
+
|
| 517 |
+
<tr>
|
| 518 |
+
<td align="left"><code>key</code></td>
|
| 519 |
+
<td align="left" style="width: 25%;">
|
| 520 |
+
|
| 521 |
+
```python
|
| 522 |
+
int | str | tuple[int | str, ...] | None
|
| 523 |
+
```
|
| 524 |
+
|
| 525 |
+
</td>
|
| 526 |
+
<td align="left"><code>None</code></td>
|
| 527 |
+
<td align="left">In a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render.</td>
|
| 528 |
+
</tr>
|
| 529 |
+
|
| 530 |
+
<tr>
|
| 531 |
+
<td align="left"><code>preserved_by_key</code></td>
|
| 532 |
+
<td align="left" style="width: 25%;">
|
| 533 |
+
|
| 534 |
+
```python
|
| 535 |
+
list[str] | str | None
|
| 536 |
+
```
|
| 537 |
+
|
| 538 |
+
</td>
|
| 539 |
+
<td align="left"><code>"value"</code></td>
|
| 540 |
+
<td align="left">A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor.</td>
|
| 541 |
+
</tr>
|
| 542 |
+
|
| 543 |
+
<tr>
|
| 544 |
+
<td align="left"><code>show_share_button</code></td>
|
| 545 |
+
<td align="left" style="width: 25%;">
|
| 546 |
+
|
| 547 |
+
```python
|
| 548 |
+
bool | None
|
| 549 |
+
```
|
| 550 |
+
|
| 551 |
+
</td>
|
| 552 |
+
<td align="left"><code>None</code></td>
|
| 553 |
+
<td align="left">If True, will show a share icon in the corner of the component that allows user to share outputs to Hugging Face Spaces Discussions. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.</td>
|
| 554 |
+
</tr>
|
| 555 |
+
|
| 556 |
+
<tr>
|
| 557 |
+
<td align="left"><code>placeholder</code></td>
|
| 558 |
+
<td align="left" style="width: 25%;">
|
| 559 |
+
|
| 560 |
+
```python
|
| 561 |
+
str | None
|
| 562 |
+
```
|
| 563 |
+
|
| 564 |
+
</td>
|
| 565 |
+
<td align="left"><code>None</code></td>
|
| 566 |
+
<td align="left">Custom text for the upload area. Overrides default upload messages when provided. Accepts new lines and `#` to designate a heading.</td>
|
| 567 |
+
</tr>
|
| 568 |
+
|
| 569 |
+
<tr>
|
| 570 |
+
<td align="left"><code>show_fullscreen_button</code></td>
|
| 571 |
+
<td align="left" style="width: 25%;">
|
| 572 |
+
|
| 573 |
+
```python
|
| 574 |
+
bool
|
| 575 |
+
```
|
| 576 |
+
|
| 577 |
+
</td>
|
| 578 |
+
<td align="left"><code>True</code></td>
|
| 579 |
+
<td align="left">If True, will show a fullscreen icon in the corner of the component that allows user to view the image in fullscreen mode. If False, icon does not appear.</td>
|
| 580 |
+
</tr>
|
| 581 |
+
|
| 582 |
+
<tr>
|
| 583 |
+
<td align="left"><code>only_custom_metadata</code></td>
|
| 584 |
+
<td align="left" style="width: 25%;">
|
| 585 |
+
|
| 586 |
+
```python
|
| 587 |
+
bool
|
| 588 |
+
```
|
| 589 |
+
|
| 590 |
+
</td>
|
| 591 |
+
<td align="left"><code>True</code></td>
|
| 592 |
+
<td align="left">If True, extracts only custom metadata, excluding technical metadata like ImageWidth or ImageHeight. Defaults to True.</td>
|
| 593 |
+
</tr>
|
| 594 |
+
|
| 595 |
+
<tr>
|
| 596 |
+
<td align="left"><code>disable_preprocess</code></td>
|
| 597 |
+
<td align="left" style="width: 25%;">
|
| 598 |
+
|
| 599 |
+
```python
|
| 600 |
+
bool
|
| 601 |
+
```
|
| 602 |
+
|
| 603 |
+
</td>
|
| 604 |
+
<td align="left"><code>True</code></td>
|
| 605 |
+
<td align="left">If True, skips preprocessing and returns the raw ImageMetaData payload. Defaults to True.</td>
|
| 606 |
+
</tr>
|
| 607 |
+
|
| 608 |
+
<tr>
|
| 609 |
+
<td align="left"><code>popup_metadata_width</code></td>
|
| 610 |
+
<td align="left" style="width: 25%;">
|
| 611 |
+
|
| 612 |
+
```python
|
| 613 |
+
int | str
|
| 614 |
+
```
|
| 615 |
+
|
| 616 |
+
</td>
|
| 617 |
+
<td align="left"><code>400</code></td>
|
| 618 |
+
<td align="left">Metadata popup width in pixels or CSS units. Defaults to 400.</td>
|
| 619 |
+
</tr>
|
| 620 |
+
|
| 621 |
+
<tr>
|
| 622 |
+
<td align="left"><code>popup_metadata_height</code></td>
|
| 623 |
+
<td align="left" style="width: 25%;">
|
| 624 |
+
|
| 625 |
+
```python
|
| 626 |
+
int | str
|
| 627 |
+
```
|
| 628 |
+
|
| 629 |
+
</td>
|
| 630 |
+
<td align="left"><code>300</code></td>
|
| 631 |
+
<td align="left">Metadata popup height in pixels or CSS units. Defaults to 300.</td>
|
| 632 |
+
</tr>
|
| 633 |
+
</tbody></table>
|
| 634 |
+
|
| 635 |
+
|
| 636 |
+
### Events
|
| 637 |
+
|
| 638 |
+
| name | description |
|
| 639 |
+
|:-----|:------------|
|
| 640 |
+
| `clear` | This listener is triggered when the user clears the ImageMeta using the clear button for the component. |
|
| 641 |
+
| `change` | Triggered when the value of the ImageMeta changes either because of user input (e.g. a user types in a textbox) OR because of a function update (e.g. an image receives a value from the output of an event trigger). See `.input()` for a listener that is only triggered by user input. |
|
| 642 |
+
| `select` | Event listener for when the user selects or deselects the ImageMeta. Uses event data gradio.SelectData to carry `value` referring to the label of the ImageMeta, and `selected` to refer to state of the ImageMeta. See EventData documentation on how to use this event data |
|
| 643 |
+
| `upload` | This listener is triggered when the user uploads a file into the ImageMeta. |
|
| 644 |
+
| `input` | This listener is triggered when the user changes the value of the ImageMeta. |
|
| 645 |
+
| `load_metadata` | Triggered when the user clicks the 'Load Metadata' button, expecting ImageMetaData as input. |
|
| 646 |
+
|
| 647 |
+
|
| 648 |
+
|
| 649 |
+
### User function
|
| 650 |
+
|
| 651 |
+
The impact on the users predict function varies depending on whether the component is used as an input or output for an event (or both).
|
| 652 |
+
|
| 653 |
+
- When used as an Input, the component only impacts the input signature of the user function.
|
| 654 |
+
- When used as an output, the component only impacts the return signature of the user function.
|
| 655 |
+
|
| 656 |
+
The code snippet below is accurate in cases where the component is used as both an input and an output.
|
| 657 |
+
|
| 658 |
+
- **As output:** Is passed, preprocessed image as a NumPy array, PIL Image, filepath, ImageMetaData, or None.
|
| 659 |
+
- **As input:** Should return, input image as a NumPy array, PIL Image, string (file path or URL), Path object, or None.
|
| 660 |
+
|
| 661 |
+
```python
|
| 662 |
+
def predict(
|
| 663 |
+
value: numpy.ndarray | PIL.Image.Image | str | ImageMetaData | None
|
| 664 |
+
) -> numpy.ndarray | PIL.Image.Image | str | pathlib.Path | None:
|
| 665 |
+
return value
|
| 666 |
+
```
|
| 667 |
+
|
| 668 |
+
|
| 669 |
+
## `ImageMetaData`
|
| 670 |
+
```python
|
| 671 |
+
class ImageMetaData(ImageData):
|
| 672 |
+
pass
|
| 673 |
+
```
|
src/backend/gradio_imagemeta/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
from .imagemeta import ImageMeta
|
| 3 |
+
|
| 4 |
+
__all__ = ['ImageMeta']
|
src/backend/gradio_imagemeta/helpers.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import fields
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from typing import Any, Dict, List
|
| 4 |
+
from PIL import Image, PngImagePlugin, ExifTags
|
| 5 |
+
import numpy as np
|
| 6 |
+
from gradio import image_utils
|
| 7 |
+
|
| 8 |
+
def extract_metadata(image_data: str | Path | Image.Image | np.ndarray | None, only_custom_metadata: bool = True) -> Dict[str, Any]:
|
| 9 |
+
"""
|
| 10 |
+
Extracts metadata from an image.
|
| 11 |
+
|
| 12 |
+
Args:
|
| 13 |
+
image_data: Image data as a filepath, Path, PIL Image, NumPy array, or None.
|
| 14 |
+
only_custom_metadata: If True, excludes technical metadata (e.g., ImageWidth, ImageHeight). Defaults to True.
|
| 15 |
+
|
| 16 |
+
Returns:
|
| 17 |
+
Dictionary of extracted metadata. Returns empty dictionary if no metadata is available or extraction fails.
|
| 18 |
+
"""
|
| 19 |
+
if not image_data:
|
| 20 |
+
return {}
|
| 21 |
+
|
| 22 |
+
try:
|
| 23 |
+
# Convert image_data to PIL.Image
|
| 24 |
+
if isinstance(image_data, (str, Path)):
|
| 25 |
+
image = Image.open(image_data)
|
| 26 |
+
elif isinstance(image_data, np.ndarray):
|
| 27 |
+
image = Image.fromarray(image_data)
|
| 28 |
+
elif isinstance(image_data, Image.Image):
|
| 29 |
+
image = image_data
|
| 30 |
+
elif hasattr(image_data, 'path'): # For ImageMetaData
|
| 31 |
+
image = Image.open(image_data.path)
|
| 32 |
+
else:
|
| 33 |
+
return {}
|
| 34 |
+
|
| 35 |
+
decoded_meta = {}
|
| 36 |
+
if image.format == "PNG":
|
| 37 |
+
if not only_custom_metadata:
|
| 38 |
+
decoded_meta["ImageWidth"] = image.width
|
| 39 |
+
decoded_meta["ImageHeight"] = image.height
|
| 40 |
+
metadata = image.info
|
| 41 |
+
if metadata:
|
| 42 |
+
for key, value in metadata.items():
|
| 43 |
+
if isinstance(value, bytes):
|
| 44 |
+
value = value.decode(errors='ignore')
|
| 45 |
+
decoded_meta[str(key)] = value
|
| 46 |
+
else:
|
| 47 |
+
exif_data = image.getexif()
|
| 48 |
+
if exif_data:
|
| 49 |
+
for tag_id, value in exif_data.items():
|
| 50 |
+
tag = ExifTags.TAGS.get(tag_id, tag_id)
|
| 51 |
+
if isinstance(value, bytes):
|
| 52 |
+
value = value.decode(errors='ignore')
|
| 53 |
+
decoded_meta[str(tag)] = value
|
| 54 |
+
if not only_custom_metadata:
|
| 55 |
+
decoded_meta["ImageWidth"] = image.width
|
| 56 |
+
decoded_meta["ImageHeight"] = image.height
|
| 57 |
+
|
| 58 |
+
return decoded_meta
|
| 59 |
+
except Exception:
|
| 60 |
+
return {}
|
| 61 |
+
|
| 62 |
+
def preprocess_image(image_data: str | Path | Image.Image | np.ndarray, type: str = "numpy") -> np.ndarray | Image.Image:
|
| 63 |
+
"""
|
| 64 |
+
Processes an image to the specified format.
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
image_data: Image data as a filepath, Path, PIL Image, or NumPy array.
|
| 68 |
+
type: Output format, either "numpy" (array with shape (height, width, 3)) or "pil" (PIL Image). Defaults to "numpy".
|
| 69 |
+
|
| 70 |
+
Returns:
|
| 71 |
+
Processed image as a NumPy array or PIL Image.
|
| 72 |
+
|
| 73 |
+
Raises:
|
| 74 |
+
ValueError: If image_data type or output type is unsupported.
|
| 75 |
+
"""
|
| 76 |
+
if isinstance(image_data, (str, Path)):
|
| 77 |
+
payload = image_data
|
| 78 |
+
elif isinstance(image_data, Image.Image):
|
| 79 |
+
payload = image_data
|
| 80 |
+
elif isinstance(image_data, np.ndarray):
|
| 81 |
+
payload = image_data
|
| 82 |
+
elif hasattr(image_data, 'path'): # For ImageMetaData
|
| 83 |
+
payload = image_data.path
|
| 84 |
+
else:
|
| 85 |
+
raise ValueError(f"Unsupported image_data type: {type(image_data)}")
|
| 86 |
+
|
| 87 |
+
if type == "numpy":
|
| 88 |
+
return image_utils.preprocess_image(payload, type="numpy")
|
| 89 |
+
elif type == "pil":
|
| 90 |
+
return image_utils.preprocess_image(payload, type="pil")
|
| 91 |
+
else:
|
| 92 |
+
raise ValueError(f"Unsupported type: {type}")
|
| 93 |
+
|
| 94 |
+
def add_metadata(image_data: str | Path | Image.Image | np.ndarray, metadata: Dict[str, Any], save_path: str) -> bool:
|
| 95 |
+
"""
|
| 96 |
+
Adds metadata to an image and saves it to the specified path.
|
| 97 |
+
|
| 98 |
+
Args:
|
| 99 |
+
image_data: Image data as a filepath, Path, PIL Image, or NumPy array.
|
| 100 |
+
metadata: Dictionary of metadata to add to the image.
|
| 101 |
+
save_path: Filepath where the modified image will be saved.
|
| 102 |
+
|
| 103 |
+
Returns:
|
| 104 |
+
True if metadata was added and image was saved successfully, False otherwise.
|
| 105 |
+
"""
|
| 106 |
+
try:
|
| 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)
|
| 113 |
+
elif isinstance(image_data, np.ndarray):
|
| 114 |
+
image = Image.fromarray(image_data)
|
| 115 |
+
elif isinstance(image_data, Image.Image):
|
| 116 |
+
image = image_data
|
| 117 |
+
elif hasattr(image_data, 'path'): # For ImageMetaData
|
| 118 |
+
image = Image.open(image_data.path)
|
| 119 |
+
else:
|
| 120 |
+
return False
|
| 121 |
+
|
| 122 |
+
image_copy = image.copy()
|
| 123 |
+
if image.format == "PNG":
|
| 124 |
+
meta = PngImagePlugin.PngInfo()
|
| 125 |
+
for key, value in metadata.items():
|
| 126 |
+
meta.add_text(str(key), str(value))
|
| 127 |
+
image_copy.info.update(metadata) # For reference, but requires pnginfo when saving
|
| 128 |
+
image_copy.save(save_path, pnginfo=meta)
|
| 129 |
+
else:
|
| 130 |
+
exif = image_copy.getexif() or Image.Exif()
|
| 131 |
+
for key, value in metadata.items():
|
| 132 |
+
tag_id = next((k for k, v in ExifTags.TAGS.items() if v == key), None)
|
| 133 |
+
if tag_id:
|
| 134 |
+
exif[tag_id] = value
|
| 135 |
+
image_copy.exif = exif
|
| 136 |
+
image_copy.save(save_path)
|
| 137 |
+
return True
|
| 138 |
+
except Exception:
|
| 139 |
+
return False
|
| 140 |
+
|
| 141 |
+
def transfer_metadata(output_fields: List[Any], metadata: Dict[str, Any], dataclass_fields: Dict[str, str]) -> List[Any]:
|
| 142 |
+
"""
|
| 143 |
+
Maps metadata to a list of output components based on their labels.
|
| 144 |
+
|
| 145 |
+
Args:
|
| 146 |
+
output_fields: List of components (e.g., Textbox, PropertySheet).
|
| 147 |
+
metadata: Dictionary of extracted image metadata.
|
| 148 |
+
dataclass_fields: Dictionary mapping component labels (e.g., 'Model') to field paths (e.g., 'image_settings.model' or 'description').
|
| 149 |
+
|
| 150 |
+
Returns:
|
| 151 |
+
List of values (strings for Textbox, nested dictionary for PropertySheet) in the same order as output_fields.
|
| 152 |
+
"""
|
| 153 |
+
output_values = [None] * len(output_fields)
|
| 154 |
+
for i, component in enumerate(output_fields):
|
| 155 |
+
label = getattr(component, 'label', None)
|
| 156 |
+
|
| 157 |
+
# Check if the component is a PropertySheet via root_label attribute
|
| 158 |
+
is_property_sheet = hasattr(component, 'root_label')
|
| 159 |
+
if is_property_sheet:
|
| 160 |
+
# Create nested dictionary for PropertySheet
|
| 161 |
+
updated_config = {}
|
| 162 |
+
for dataclass_label, field_path in dataclass_fields.items():
|
| 163 |
+
value = str(metadata.get(dataclass_label, 'None'))
|
| 164 |
+
value = None if value == 'None' else value
|
| 165 |
+
# Split field_path into parts (e.g., 'image_settings.model' -> ['image_settings', 'model'])
|
| 166 |
+
parts = field_path.split('.')
|
| 167 |
+
# Build nested structure in dictionary
|
| 168 |
+
current = updated_config
|
| 169 |
+
for part in parts[:-1]:
|
| 170 |
+
if part not in current:
|
| 171 |
+
current[part] = {}
|
| 172 |
+
current = current[part]
|
| 173 |
+
# Assign value to final field
|
| 174 |
+
current[parts[-1]] = value
|
| 175 |
+
output_values[i] = updated_config
|
| 176 |
+
else:
|
| 177 |
+
# For other components (e.g., Textbox), assign raw value
|
| 178 |
+
value = str(metadata.get(label, None)) if label else None
|
| 179 |
+
value = None if value == 'None' else value
|
| 180 |
+
output_values[i] = value
|
| 181 |
+
|
| 182 |
+
return output_values
|
src/backend/gradio_imagemeta/imagemeta.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
from collections.abc import Callable, Sequence
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from PIL import Image
|
| 5 |
+
import numpy as np
|
| 6 |
+
from typing import TYPE_CHECKING, Any, Literal
|
| 7 |
+
from gradio_client import handle_file
|
| 8 |
+
from gradio.components.base import Component
|
| 9 |
+
from gradio.data_classes import Base64ImageData, ImageData
|
| 10 |
+
from gradio.events import Events, EventListener
|
| 11 |
+
from gradio import image_utils, utils
|
| 12 |
+
from gradio.i18n import I18nData
|
| 13 |
+
|
| 14 |
+
if TYPE_CHECKING:
|
| 15 |
+
from gradio.components import Timer
|
| 16 |
+
|
| 17 |
+
Image.init()
|
| 18 |
+
|
| 19 |
+
class ImageMetaData(ImageData):
|
| 20 |
+
"""Custom data model for image data with metadata support."""
|
| 21 |
+
pass
|
| 22 |
+
|
| 23 |
+
class ImageMeta(Component):
|
| 24 |
+
"""
|
| 25 |
+
A Gradio component for uploading or displaying images, with support for metadata extraction and a custom load_metadata event.
|
| 26 |
+
|
| 27 |
+
This component allows users to upload images (as input) or display images (as output). It includes a custom event for loading metadata, triggered by a 'Load Metadata' button in the UI, which expects ImageMetaData as input.
|
| 28 |
+
"""
|
| 29 |
+
load_metadata = EventListener(
|
| 30 |
+
"load_metadata",
|
| 31 |
+
doc="Triggered when the user clicks the 'Load Metadata' button, expecting ImageMetaData as input."
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
EVENTS = [
|
| 35 |
+
Events.clear,
|
| 36 |
+
Events.change,
|
| 37 |
+
Events.select,
|
| 38 |
+
Events.upload,
|
| 39 |
+
Events.input,
|
| 40 |
+
load_metadata,
|
| 41 |
+
]
|
| 42 |
+
|
| 43 |
+
data_model = ImageMetaData
|
| 44 |
+
image_mode: Literal["1", "L", "P", "RGB", "RGBA", "CMYK", "YCbCr", "LAB", "HSV", "I", "F"] | None
|
| 45 |
+
type: Literal["numpy", "pil", "filepath"]
|
| 46 |
+
|
| 47 |
+
def __init__(
|
| 48 |
+
self,
|
| 49 |
+
value: str | Image.Image | np.ndarray | Callable | None = None,
|
| 50 |
+
*,
|
| 51 |
+
format: str = "webp",
|
| 52 |
+
height: int | str | None = None,
|
| 53 |
+
width: int | str | None = None,
|
| 54 |
+
image_mode: Literal["1", "L", "P", "RGB", "RGBA", "CMYK", "YCbCr", "LAB", "HSV", "I", "F"] | None = "RGB",
|
| 55 |
+
type: Literal["numpy", "pil", "filepath"] = "numpy",
|
| 56 |
+
label: str | I18nData | None = None,
|
| 57 |
+
every: Timer | float | None = None,
|
| 58 |
+
inputs: Component | Sequence[Component] | set[Component] | None = None,
|
| 59 |
+
show_label: bool | None = None,
|
| 60 |
+
show_download_button: bool = True,
|
| 61 |
+
container: bool = True,
|
| 62 |
+
scale: int | None = None,
|
| 63 |
+
min_width: int = 160,
|
| 64 |
+
interactive: bool | None = None,
|
| 65 |
+
visible: bool = True,
|
| 66 |
+
elem_id: str | None = None,
|
| 67 |
+
elem_classes: list[str] | str | None = None,
|
| 68 |
+
render: bool = True,
|
| 69 |
+
key: int | str | tuple[int | str, ...] | None = None,
|
| 70 |
+
preserved_by_key: list[str] | str | None = "value",
|
| 71 |
+
show_share_button: bool | None = None,
|
| 72 |
+
placeholder: str | None = None,
|
| 73 |
+
show_fullscreen_button: bool = True,
|
| 74 |
+
only_custom_metadata: bool = True,
|
| 75 |
+
disable_preprocess: bool = True,
|
| 76 |
+
popup_metadata_width: int | str = 400,
|
| 77 |
+
popup_metadata_height: int | str = 300,
|
| 78 |
+
):
|
| 79 |
+
"""
|
| 80 |
+
Initializes the ImageMeta component.
|
| 81 |
+
|
| 82 |
+
Args:
|
| 83 |
+
value: A PIL Image, numpy array, path or URL for the default value that Image component is going to take. If a function is provided, the function will be called each time the app loads to set the initial value of this component.
|
| 84 |
+
format: File format (e.g. "png" or "gif"). Used to save image if it does not already have a valid format (e.g. if the image is being returned to the frontend as a numpy array or PIL Image). The format should be supported by the PIL library. Applies both when this component is used as an input or output. This parameter has no effect on SVG files.
|
| 85 |
+
height: The height of the component, specified in pixels if a number is passed, or in CSS units if a string is passed. This has no effect on the preprocessed image file or numpy array, but will affect the displayed image.
|
| 86 |
+
width: The width of the component, specified in pixels if a number is passed, or in CSS units if a string is passed. This has no effect on the preprocessed image file or numpy array, but will affect the displayed image.
|
| 87 |
+
image_mode: The pixel format and color depth that the image should be loaded and preprocessed as. "RGB" will load the image as a color image, or "L" as black-and-white. See https://pillow.readthedocs.io/en/stable/handbook/concepts.html for other supported image modes and their meaning. This parameter has no effect on SVG or GIF files. If set to None, the image_mode will be inferred from the image file type (e.g. "RGBA" for a .png image, "RGB" in most other cases).
|
| 88 |
+
type: The format the image is converted before being passed into the prediction function. "numpy" converts the image to a numpy array with shape (height, width, 3) and values from 0 to 255, "pil" converts the image to a PIL image object, "filepath" passes a str path to a temporary file containing the image. To support animated GIFs in input, the `type` should be set to "filepath" or "pil". To support SVGs, the `type` should be set to "filepath".
|
| 89 |
+
label: The label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.
|
| 90 |
+
every: Continously calls `value` to recalculate it if `value` is a function (has no effect otherwise). Can provide a Timer whose tick resets `value`, or a float that provides the regular interval for the reset Timer.
|
| 91 |
+
inputs: Components that are used as inputs to calculate `value` if `value` is a function (has no effect otherwise). `value` is recalculated any time the inputs change.
|
| 92 |
+
show_label: If True, will display label.
|
| 93 |
+
show_download_button: If True, will display button to download image. Only applies if interactive is False (e.g. if the component is used as an output).
|
| 94 |
+
container: If True, will place the component in a container - providing some extra padding around the border.
|
| 95 |
+
scale: Relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.
|
| 96 |
+
min_width: Minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.
|
| 97 |
+
interactive: If True, will allow users to upload and edit an image; if False, can only be used to display images. If not provided, this is inferred based on whether the component is used as an input or output.
|
| 98 |
+
visible: If False, component will be hidden.
|
| 99 |
+
elem_id: An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.
|
| 100 |
+
elem_classes: An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.
|
| 101 |
+
render: If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.
|
| 102 |
+
key: In a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render.
|
| 103 |
+
preserved_by_key: A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor.
|
| 104 |
+
show_share_button: If True, will show a share icon in the corner of the component that allows user to share outputs to Hugging Face Spaces Discussions. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.
|
| 105 |
+
placeholder: Custom text for the upload area. Overrides default upload messages when provided. Accepts new lines and `#` to designate a heading.
|
| 106 |
+
show_fullscreen_button: If True, will show a fullscreen icon in the corner of the component that allows user to view the image in fullscreen mode. If False, icon does not appear.
|
| 107 |
+
only_custom_metadata: If True, extracts only custom metadata, excluding technical metadata like ImageWidth or ImageHeight. Defaults to True.
|
| 108 |
+
disable_preprocess: If True, skips preprocessing and returns the raw ImageMetaData payload. Defaults to True.
|
| 109 |
+
popup_metadata_width: Metadata popup width in pixels or CSS units. Defaults to 400.
|
| 110 |
+
popup_metadata_height: Metadata popup height in pixels or CSS units. Defaults to 300.
|
| 111 |
+
"""
|
| 112 |
+
self.format = format
|
| 113 |
+
self.height = height
|
| 114 |
+
self.width = width
|
| 115 |
+
self.image_mode = image_mode
|
| 116 |
+
valid_types = ["numpy", "pil", "filepath"]
|
| 117 |
+
if type not in valid_types:
|
| 118 |
+
raise ValueError(f"Invalid value for parameter `type`: {type}. Please choose from one of: {valid_types}")
|
| 119 |
+
self.type = type
|
| 120 |
+
self.show_download_button = show_download_button
|
| 121 |
+
self.show_share_button = (utils.get_space() is not None) if show_share_button is None else show_share_button
|
| 122 |
+
self.show_fullscreen_button = show_fullscreen_button
|
| 123 |
+
self.placeholder = placeholder
|
| 124 |
+
self.only_custom_metadata = only_custom_metadata
|
| 125 |
+
self.disable_preprocess = disable_preprocess
|
| 126 |
+
self.popup_metadata_width = popup_metadata_width
|
| 127 |
+
self.popup_metadata_height = popup_metadata_height
|
| 128 |
+
|
| 129 |
+
super().__init__(
|
| 130 |
+
label=label,
|
| 131 |
+
every=every,
|
| 132 |
+
inputs=inputs,
|
| 133 |
+
show_label=show_label,
|
| 134 |
+
container=container,
|
| 135 |
+
scale=scale,
|
| 136 |
+
min_width=min_width,
|
| 137 |
+
interactive=interactive,
|
| 138 |
+
visible=visible,
|
| 139 |
+
elem_id=elem_id,
|
| 140 |
+
elem_classes=elem_classes,
|
| 141 |
+
render=render,
|
| 142 |
+
key=key,
|
| 143 |
+
preserved_by_key=preserved_by_key,
|
| 144 |
+
value=value,
|
| 145 |
+
)
|
| 146 |
+
self._value_description = (
|
| 147 |
+
"a filepath to an image" if self.type == "filepath" else
|
| 148 |
+
("a numpy array representing an image" if self.type == "numpy" else "a PIL Image")
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
def preprocess(
|
| 152 |
+
self, payload: ImageMetaData | None
|
| 153 |
+
) -> np.ndarray | Image.Image | str | ImageMetaData | None:
|
| 154 |
+
"""
|
| 155 |
+
Preprocesses the input image data for use in the application.
|
| 156 |
+
|
| 157 |
+
Args:
|
| 158 |
+
payload: ImageMetaData object containing image data and metadata, or None.
|
| 159 |
+
|
| 160 |
+
Returns:
|
| 161 |
+
Preprocessed image as a NumPy array, PIL Image, filepath, ImageMetaData, or None.
|
| 162 |
+
"""
|
| 163 |
+
if payload is None or not hasattr(payload, "path"):
|
| 164 |
+
return None
|
| 165 |
+
|
| 166 |
+
if self.disable_preprocess:
|
| 167 |
+
return payload
|
| 168 |
+
|
| 169 |
+
return image_utils.preprocess_image(
|
| 170 |
+
payload,
|
| 171 |
+
cache_dir=self.GRADIO_CACHE,
|
| 172 |
+
format=self.format,
|
| 173 |
+
image_mode=self.image_mode,
|
| 174 |
+
type=self.type,
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
def postprocess(
|
| 178 |
+
self, value: np.ndarray | Image.Image | str | Path | None
|
| 179 |
+
) -> ImageMetaData | Base64ImageData | None:
|
| 180 |
+
"""
|
| 181 |
+
Post-processes the input image to prepare it for rendering, preserving the original format when interactive=False.
|
| 182 |
+
|
| 183 |
+
Args:
|
| 184 |
+
value: Input image as a NumPy array, PIL Image, string (file path or URL), Path object, or None.
|
| 185 |
+
|
| 186 |
+
Returns:
|
| 187 |
+
Processed image data as ImageMetaData, Base64ImageData, or None.
|
| 188 |
+
"""
|
| 189 |
+
format = self.format
|
| 190 |
+
if not self.interactive:
|
| 191 |
+
if isinstance(value, Image.Image) and value.format:
|
| 192 |
+
format = value.format.lower()
|
| 193 |
+
elif isinstance(value, (str, Path)):
|
| 194 |
+
ext = str(value).rsplit(".", 1)[-1].lower() if "." in str(value) else None
|
| 195 |
+
if ext in ["png", "jpg", "jpeg", "webp", "gif"]:
|
| 196 |
+
format = ext
|
| 197 |
+
|
| 198 |
+
return image_utils.postprocess_image(
|
| 199 |
+
value,
|
| 200 |
+
cache_dir=self.GRADIO_CACHE,
|
| 201 |
+
format=format,
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
def api_info_as_output(self) -> dict[str, Any]:
|
| 205 |
+
"""
|
| 206 |
+
Provides API information for the component as an output.
|
| 207 |
+
|
| 208 |
+
Returns:
|
| 209 |
+
A dictionary containing API metadata for the component.
|
| 210 |
+
"""
|
| 211 |
+
return self.api_info()
|
| 212 |
+
|
| 213 |
+
def example_payload(self) -> Any:
|
| 214 |
+
"""
|
| 215 |
+
Returns an example payload for testing the component.
|
| 216 |
+
|
| 217 |
+
Returns:
|
| 218 |
+
A processed file object for a sample image.
|
| 219 |
+
"""
|
| 220 |
+
return handle_file(
|
| 221 |
+
"https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png"
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
def example_value(self) -> Any:
|
| 225 |
+
"""
|
| 226 |
+
Returns an example value for the component.
|
| 227 |
+
|
| 228 |
+
Returns:
|
| 229 |
+
A URL to a sample image.
|
| 230 |
+
"""
|
| 231 |
+
return "https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png"
|
src/backend/gradio_imagemeta/templates/component/index.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
src/backend/gradio_imagemeta/templates/component/style.css
ADDED
|
@@ -0,0 +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}
|
src/backend/gradio_imagemeta/templates/example/index.js
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { setContext: ie, getContext: E } = window.__gradio__svelte__internal, C = "WORKER_PROXY_CONTEXT_KEY";
|
| 2 |
+
function k() {
|
| 3 |
+
return E(C);
|
| 4 |
+
}
|
| 5 |
+
const R = "lite.local";
|
| 6 |
+
function O(n) {
|
| 7 |
+
return n.host === window.location.host || n.host === "localhost:7860" || n.host === "127.0.0.1:7860" || // Ref: https://github.com/gradio-app/gradio/blob/v3.32.0/js/app/src/Index.svelte#L194
|
| 8 |
+
n.host === R;
|
| 9 |
+
}
|
| 10 |
+
function q(n, e) {
|
| 11 |
+
const r = e.toLowerCase();
|
| 12 |
+
for (const [t, o] of Object.entries(n))
|
| 13 |
+
if (t.toLowerCase() === r)
|
| 14 |
+
return o;
|
| 15 |
+
}
|
| 16 |
+
function L(n) {
|
| 17 |
+
const e = typeof window < "u";
|
| 18 |
+
if (n == null || !e)
|
| 19 |
+
return !1;
|
| 20 |
+
const r = new URL(n, window.location.href);
|
| 21 |
+
return !(!O(r) || r.protocol !== "http:" && r.protocol !== "https:");
|
| 22 |
+
}
|
| 23 |
+
let u;
|
| 24 |
+
async function T(n) {
|
| 25 |
+
const e = typeof window < "u";
|
| 26 |
+
if (n == null || !e || !L(n))
|
| 27 |
+
return n;
|
| 28 |
+
if (u == null)
|
| 29 |
+
try {
|
| 30 |
+
u = k();
|
| 31 |
+
} catch {
|
| 32 |
+
return n;
|
| 33 |
+
}
|
| 34 |
+
if (u == null)
|
| 35 |
+
return n;
|
| 36 |
+
const t = new URL(n, window.location.href).pathname;
|
| 37 |
+
return u.httpRequest({
|
| 38 |
+
method: "GET",
|
| 39 |
+
path: t,
|
| 40 |
+
headers: {},
|
| 41 |
+
query_string: ""
|
| 42 |
+
}).then((o) => {
|
| 43 |
+
if (o.status !== 200)
|
| 44 |
+
throw new Error(`Failed to get file ${t} from the Wasm worker.`);
|
| 45 |
+
const l = new Blob([o.body], {
|
| 46 |
+
type: q(o.headers, "content-type")
|
| 47 |
+
});
|
| 48 |
+
return URL.createObjectURL(l);
|
| 49 |
+
});
|
| 50 |
+
}
|
| 51 |
+
const {
|
| 52 |
+
SvelteComponent: ae,
|
| 53 |
+
assign: _e,
|
| 54 |
+
check_outros: ce,
|
| 55 |
+
children: ue,
|
| 56 |
+
claim_element: fe,
|
| 57 |
+
compute_rest_props: de,
|
| 58 |
+
create_slot: me,
|
| 59 |
+
detach: he,
|
| 60 |
+
element: ge,
|
| 61 |
+
empty: pe,
|
| 62 |
+
exclude_internal_props: we,
|
| 63 |
+
get_all_dirty_from_scope: be,
|
| 64 |
+
get_slot_changes: ye,
|
| 65 |
+
get_spread_update: ve,
|
| 66 |
+
group_outros: Ee,
|
| 67 |
+
init: Ce,
|
| 68 |
+
insert_hydration: ke,
|
| 69 |
+
listen: Re,
|
| 70 |
+
prevent_default: Oe,
|
| 71 |
+
safe_not_equal: qe,
|
| 72 |
+
set_attributes: Le,
|
| 73 |
+
set_style: Te,
|
| 74 |
+
toggle_class: Ke,
|
| 75 |
+
transition_in: Se,
|
| 76 |
+
transition_out: Ue,
|
| 77 |
+
update_slot_base: Pe
|
| 78 |
+
} = window.__gradio__svelte__internal, { createEventDispatcher: We, onMount: $e } = window.__gradio__svelte__internal, {
|
| 79 |
+
SvelteComponent: K,
|
| 80 |
+
assign: d,
|
| 81 |
+
bubble: S,
|
| 82 |
+
claim_element: U,
|
| 83 |
+
compute_rest_props: h,
|
| 84 |
+
detach: P,
|
| 85 |
+
element: W,
|
| 86 |
+
exclude_internal_props: $,
|
| 87 |
+
get_spread_update: I,
|
| 88 |
+
init: X,
|
| 89 |
+
insert_hydration: Y,
|
| 90 |
+
listen: j,
|
| 91 |
+
noop: g,
|
| 92 |
+
safe_not_equal: D,
|
| 93 |
+
set_attributes: p,
|
| 94 |
+
src_url_equal: F,
|
| 95 |
+
toggle_class: w
|
| 96 |
+
} = window.__gradio__svelte__internal;
|
| 97 |
+
function G(n) {
|
| 98 |
+
let e, r, t, o, l = [
|
| 99 |
+
{
|
| 100 |
+
src: r = /*resolved_src*/
|
| 101 |
+
n[0]
|
| 102 |
+
},
|
| 103 |
+
/*$$restProps*/
|
| 104 |
+
n[1]
|
| 105 |
+
], s = {};
|
| 106 |
+
for (let i = 0; i < l.length; i += 1)
|
| 107 |
+
s = d(s, l[i]);
|
| 108 |
+
return {
|
| 109 |
+
c() {
|
| 110 |
+
e = W("img"), this.h();
|
| 111 |
+
},
|
| 112 |
+
l(i) {
|
| 113 |
+
e = U(i, "IMG", { src: !0 }), this.h();
|
| 114 |
+
},
|
| 115 |
+
h() {
|
| 116 |
+
p(e, s), w(e, "svelte-kxeri3", !0);
|
| 117 |
+
},
|
| 118 |
+
m(i, c) {
|
| 119 |
+
Y(i, e, c), t || (o = j(
|
| 120 |
+
e,
|
| 121 |
+
"load",
|
| 122 |
+
/*load_handler*/
|
| 123 |
+
n[4]
|
| 124 |
+
), t = !0);
|
| 125 |
+
},
|
| 126 |
+
p(i, [c]) {
|
| 127 |
+
p(e, s = I(l, [
|
| 128 |
+
c & /*resolved_src*/
|
| 129 |
+
1 && !F(e.src, r = /*resolved_src*/
|
| 130 |
+
i[0]) && { src: r },
|
| 131 |
+
c & /*$$restProps*/
|
| 132 |
+
2 && /*$$restProps*/
|
| 133 |
+
i[1]
|
| 134 |
+
])), w(e, "svelte-kxeri3", !0);
|
| 135 |
+
},
|
| 136 |
+
i: g,
|
| 137 |
+
o: g,
|
| 138 |
+
d(i) {
|
| 139 |
+
i && P(e), t = !1, o();
|
| 140 |
+
}
|
| 141 |
+
};
|
| 142 |
+
}
|
| 143 |
+
function H(n, e, r) {
|
| 144 |
+
const t = ["src"];
|
| 145 |
+
let o = h(e, t), { src: l = void 0 } = e, s, i;
|
| 146 |
+
function c(a) {
|
| 147 |
+
S.call(this, n, a);
|
| 148 |
+
}
|
| 149 |
+
return n.$$set = (a) => {
|
| 150 |
+
e = d(d({}, e), $(a)), r(1, o = h(e, t)), "src" in a && r(2, l = a.src);
|
| 151 |
+
}, n.$$.update = () => {
|
| 152 |
+
if (n.$$.dirty & /*src, latest_src*/
|
| 153 |
+
12) {
|
| 154 |
+
r(0, s = l), r(3, i = l);
|
| 155 |
+
const a = l;
|
| 156 |
+
T(a).then((v) => {
|
| 157 |
+
i === a && r(0, s = v);
|
| 158 |
+
});
|
| 159 |
+
}
|
| 160 |
+
}, [s, o, l, i, c];
|
| 161 |
+
}
|
| 162 |
+
class M extends K {
|
| 163 |
+
constructor(e) {
|
| 164 |
+
super(), X(this, e, H, G, D, { src: 2 });
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
const {
|
| 168 |
+
SvelteComponent: N,
|
| 169 |
+
attr: V,
|
| 170 |
+
check_outros: x,
|
| 171 |
+
children: A,
|
| 172 |
+
claim_component: B,
|
| 173 |
+
claim_element: z,
|
| 174 |
+
create_component: J,
|
| 175 |
+
destroy_component: Q,
|
| 176 |
+
detach: b,
|
| 177 |
+
element: Z,
|
| 178 |
+
group_outros: ee,
|
| 179 |
+
init: te,
|
| 180 |
+
insert_hydration: ne,
|
| 181 |
+
mount_component: oe,
|
| 182 |
+
safe_not_equal: re,
|
| 183 |
+
toggle_class: _,
|
| 184 |
+
transition_in: f,
|
| 185 |
+
transition_out: m
|
| 186 |
+
} = window.__gradio__svelte__internal;
|
| 187 |
+
function y(n) {
|
| 188 |
+
let e, r;
|
| 189 |
+
return e = new M({
|
| 190 |
+
props: { src: (
|
| 191 |
+
/*value*/
|
| 192 |
+
n[0].url
|
| 193 |
+
), alt: "" }
|
| 194 |
+
}), {
|
| 195 |
+
c() {
|
| 196 |
+
J(e.$$.fragment);
|
| 197 |
+
},
|
| 198 |
+
l(t) {
|
| 199 |
+
B(e.$$.fragment, t);
|
| 200 |
+
},
|
| 201 |
+
m(t, o) {
|
| 202 |
+
oe(e, t, o), r = !0;
|
| 203 |
+
},
|
| 204 |
+
p(t, o) {
|
| 205 |
+
const l = {};
|
| 206 |
+
o & /*value*/
|
| 207 |
+
1 && (l.src = /*value*/
|
| 208 |
+
t[0].url), e.$set(l);
|
| 209 |
+
},
|
| 210 |
+
i(t) {
|
| 211 |
+
r || (f(e.$$.fragment, t), r = !0);
|
| 212 |
+
},
|
| 213 |
+
o(t) {
|
| 214 |
+
m(e.$$.fragment, t), r = !1;
|
| 215 |
+
},
|
| 216 |
+
d(t) {
|
| 217 |
+
Q(e, t);
|
| 218 |
+
}
|
| 219 |
+
};
|
| 220 |
+
}
|
| 221 |
+
function le(n) {
|
| 222 |
+
let e, r, t = (
|
| 223 |
+
/*value*/
|
| 224 |
+
n[0] && y(n)
|
| 225 |
+
);
|
| 226 |
+
return {
|
| 227 |
+
c() {
|
| 228 |
+
e = Z("div"), t && t.c(), this.h();
|
| 229 |
+
},
|
| 230 |
+
l(o) {
|
| 231 |
+
e = z(o, "DIV", { class: !0 });
|
| 232 |
+
var l = A(e);
|
| 233 |
+
t && t.l(l), l.forEach(b), this.h();
|
| 234 |
+
},
|
| 235 |
+
h() {
|
| 236 |
+
V(e, "class", "container svelte-1sgcyba"), _(
|
| 237 |
+
e,
|
| 238 |
+
"table",
|
| 239 |
+
/*type*/
|
| 240 |
+
n[1] === "table"
|
| 241 |
+
), _(
|
| 242 |
+
e,
|
| 243 |
+
"gallery",
|
| 244 |
+
/*type*/
|
| 245 |
+
n[1] === "gallery"
|
| 246 |
+
), _(
|
| 247 |
+
e,
|
| 248 |
+
"selected",
|
| 249 |
+
/*selected*/
|
| 250 |
+
n[2]
|
| 251 |
+
), _(
|
| 252 |
+
e,
|
| 253 |
+
"border",
|
| 254 |
+
/*value*/
|
| 255 |
+
n[0]
|
| 256 |
+
);
|
| 257 |
+
},
|
| 258 |
+
m(o, l) {
|
| 259 |
+
ne(o, e, l), t && t.m(e, null), r = !0;
|
| 260 |
+
},
|
| 261 |
+
p(o, [l]) {
|
| 262 |
+
/*value*/
|
| 263 |
+
o[0] ? t ? (t.p(o, l), l & /*value*/
|
| 264 |
+
1 && f(t, 1)) : (t = y(o), t.c(), f(t, 1), t.m(e, null)) : t && (ee(), m(t, 1, 1, () => {
|
| 265 |
+
t = null;
|
| 266 |
+
}), x()), (!r || l & /*type*/
|
| 267 |
+
2) && _(
|
| 268 |
+
e,
|
| 269 |
+
"table",
|
| 270 |
+
/*type*/
|
| 271 |
+
o[1] === "table"
|
| 272 |
+
), (!r || l & /*type*/
|
| 273 |
+
2) && _(
|
| 274 |
+
e,
|
| 275 |
+
"gallery",
|
| 276 |
+
/*type*/
|
| 277 |
+
o[1] === "gallery"
|
| 278 |
+
), (!r || l & /*selected*/
|
| 279 |
+
4) && _(
|
| 280 |
+
e,
|
| 281 |
+
"selected",
|
| 282 |
+
/*selected*/
|
| 283 |
+
o[2]
|
| 284 |
+
), (!r || l & /*value*/
|
| 285 |
+
1) && _(
|
| 286 |
+
e,
|
| 287 |
+
"border",
|
| 288 |
+
/*value*/
|
| 289 |
+
o[0]
|
| 290 |
+
);
|
| 291 |
+
},
|
| 292 |
+
i(o) {
|
| 293 |
+
r || (f(t), r = !0);
|
| 294 |
+
},
|
| 295 |
+
o(o) {
|
| 296 |
+
m(t), r = !1;
|
| 297 |
+
},
|
| 298 |
+
d(o) {
|
| 299 |
+
o && b(e), t && t.d();
|
| 300 |
+
}
|
| 301 |
+
};
|
| 302 |
+
}
|
| 303 |
+
function se(n, e, r) {
|
| 304 |
+
let { value: t } = e, { type: o } = e, { selected: l = !1 } = e;
|
| 305 |
+
return n.$$set = (s) => {
|
| 306 |
+
"value" in s && r(0, t = s.value), "type" in s && r(1, o = s.type), "selected" in s && r(2, l = s.selected);
|
| 307 |
+
}, [t, o, l];
|
| 308 |
+
}
|
| 309 |
+
class Ie extends N {
|
| 310 |
+
constructor(e) {
|
| 311 |
+
super(), te(this, e, se, le, re, { value: 0, type: 1, selected: 2 });
|
| 312 |
+
}
|
| 313 |
+
}
|
| 314 |
+
export {
|
| 315 |
+
Ie as default
|
| 316 |
+
};
|
src/backend/gradio_imagemeta/templates/example/style.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
.unstyled-link.svelte-151nsdd{all:unset;cursor:pointer}img.svelte-kxeri3{object-fit:cover}.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/__init__.py
ADDED
|
File without changes
|
src/demo/app.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
output_dir = Path("outputs")
|
| 11 |
+
output_dir.mkdir(exist_ok=True)
|
| 12 |
+
|
| 13 |
+
@dataclass
|
| 14 |
+
class ImageSettings:
|
| 15 |
+
"""Configuration for image metadata settings."""
|
| 16 |
+
model: str = field(default="", metadata={"label": "Model"})
|
| 17 |
+
f_number: str = field(default="", metadata={"label": "FNumber"})
|
| 18 |
+
iso_speed_ratings: str = field(default="", metadata={"label": "ISOSpeedRatings"})
|
| 19 |
+
s_churn: float = field(
|
| 20 |
+
default=0.0,
|
| 21 |
+
metadata={"component": "slider", "label": "Schurn", "minimum": 0.0, "maximum": 1.0, "step": 0.01},
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
@dataclass
|
| 25 |
+
class PropertyConfig:
|
| 26 |
+
"""Root configuration for image properties, including nested image settings."""
|
| 27 |
+
image_settings: ImageSettings = field(default_factory=ImageSettings)
|
| 28 |
+
description: str = field(default="", metadata={"label": "Description"})
|
| 29 |
+
|
| 30 |
+
def process_example_images(img_custom_path: str, img_all_path: str) -> tuple[str, str]:
|
| 31 |
+
"""
|
| 32 |
+
Processes example image paths for display in ImageMeta components.
|
| 33 |
+
|
| 34 |
+
Args:
|
| 35 |
+
img_custom_path: File path for the image to display in img_custom.
|
| 36 |
+
img_all_path: File path for the image to display in img_all.
|
| 37 |
+
|
| 38 |
+
Returns:
|
| 39 |
+
Tuple of file paths for img_custom and img_all outputs.
|
| 40 |
+
"""
|
| 41 |
+
# Verify file existence
|
| 42 |
+
if not Path(img_custom_path.path).is_file():
|
| 43 |
+
raise FileNotFoundError(f"Image not found: {img_custom_path}")
|
| 44 |
+
if not Path(img_all_path.path).is_file():
|
| 45 |
+
raise FileNotFoundError(f"Image not found: {img_all_path}")
|
| 46 |
+
|
| 47 |
+
# Return file paths as strings (ImageMeta accepts file paths as input)
|
| 48 |
+
return img_custom_path.path, img_all_path.path
|
| 49 |
+
|
| 50 |
+
def handle_load_metadata(image_data: ImageMeta | None) -> List[Any]:
|
| 51 |
+
"""
|
| 52 |
+
Processes image metadata and maps it to output components.
|
| 53 |
+
|
| 54 |
+
Args:
|
| 55 |
+
image_data: ImageMeta object containing image data and metadata, or None.
|
| 56 |
+
|
| 57 |
+
Returns:
|
| 58 |
+
A list of values for output components (Textbox, Slider, or PropertySheet instances).
|
| 59 |
+
"""
|
| 60 |
+
if not image_data:
|
| 61 |
+
return [gr.Textbox(value="") for _ in output_fields]
|
| 62 |
+
|
| 63 |
+
metadata = extract_metadata(image_data, only_custom_metadata=True)
|
| 64 |
+
dataclass_fields = build_dataclass_fields(PropertyConfig)
|
| 65 |
+
raw_values = transfer_metadata(output_fields, metadata, dataclass_fields)
|
| 66 |
+
|
| 67 |
+
output_values = [gr.skip()] * len(output_fields)
|
| 68 |
+
for i, (component, value) in enumerate(zip(output_fields, raw_values)):
|
| 69 |
+
if hasattr(component, 'root_label'):
|
| 70 |
+
output_values[i] = create_dataclass_instance(PropertyConfig, value)
|
| 71 |
+
else:
|
| 72 |
+
output_values[i] = gr.Textbox(value=value)
|
| 73 |
+
|
| 74 |
+
return output_values
|
| 75 |
+
|
| 76 |
+
def save_image_with_metadata(image_data: Any, *inputs: Any) -> str | None:
|
| 77 |
+
"""
|
| 78 |
+
Saves an image with updated metadata to a file.
|
| 79 |
+
|
| 80 |
+
Args:
|
| 81 |
+
image_data: Input image data (e.g., file path or PIL Image).
|
| 82 |
+
*inputs: Variable number of input values from UI components (Textbox, Slider).
|
| 83 |
+
|
| 84 |
+
Returns:
|
| 85 |
+
The file path of the saved image, or None if no image is provided.
|
| 86 |
+
"""
|
| 87 |
+
if not image_data:
|
| 88 |
+
return None
|
| 89 |
+
|
| 90 |
+
params = list(inputs)
|
| 91 |
+
image_params = dict(zip(input_fields.keys(), params))
|
| 92 |
+
dataclass_fields = build_dataclass_fields(PropertyConfig)
|
| 93 |
+
metadata = {label: image_params.get(label, "") for label in dataclass_fields.keys()}
|
| 94 |
+
|
| 95 |
+
new_filepath = output_dir / "image_with_meta.png"
|
| 96 |
+
add_metadata(image_data, metadata, new_filepath)
|
| 97 |
+
|
| 98 |
+
return str(new_filepath)
|
| 99 |
+
|
| 100 |
+
initial_property_from_meta_config = PropertyConfig()
|
| 101 |
+
|
| 102 |
+
with gr.Blocks() as demo:
|
| 103 |
+
gr.Markdown("# ImageMeta Component Demo")
|
| 104 |
+
gr.Markdown(
|
| 105 |
+
"""
|
| 106 |
+
**To Test:**
|
| 107 |
+
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.
|
| 108 |
+
2. Click the 'Info' icon (ⓘ) in the top-left of the image component to view the metadata panel.
|
| 109 |
+
3. Click 'Load Metadata' in the popup to populate the fields below with metadata values (`Model`, `FNumber`, `ISOSpeedRatings`, `Schurn`, `Description`).
|
| 110 |
+
4. The section below displays how metadata is rendered in components and the `PropertySheet` custom component, showing the hierarchical structure of the image settings.
|
| 111 |
+
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.
|
| 112 |
+
"""
|
| 113 |
+
)
|
| 114 |
+
property_sheet_state = gr.State(value=initial_property_from_meta_config)
|
| 115 |
+
with gr.Row():
|
| 116 |
+
img_custom = ImageMeta(
|
| 117 |
+
label="Upload Image (Custom metadata only)",
|
| 118 |
+
type="filepath",
|
| 119 |
+
width=300,
|
| 120 |
+
height=400,
|
| 121 |
+
interactive=False
|
| 122 |
+
)
|
| 123 |
+
img_all = ImageMeta(
|
| 124 |
+
label="Upload Image (All metadata)",
|
| 125 |
+
only_custom_metadata=False,
|
| 126 |
+
type="filepath",
|
| 127 |
+
width=300,
|
| 128 |
+
height=400,
|
| 129 |
+
popup_metadata_height=400,
|
| 130 |
+
popup_metadata_width=500,
|
| 131 |
+
interactive=False
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
gr.Markdown("## Metadata Viewer")
|
| 135 |
+
gr.Markdown("### Individual Components")
|
| 136 |
+
with gr.Row():
|
| 137 |
+
model_box = gr.Textbox(label="Model")
|
| 138 |
+
fnumber_box = gr.Textbox(label="FNumber")
|
| 139 |
+
iso_box = gr.Textbox(label="ISOSpeedRatings")
|
| 140 |
+
s_churn = gr.Slider(label="Schurn", value=1.0, minimum=0.0, maximum=1.0, step=0.1)
|
| 141 |
+
description_box = gr.Textbox(label="Description")
|
| 142 |
+
|
| 143 |
+
gr.Markdown("### PropertySheet Component")
|
| 144 |
+
with gr.Row():
|
| 145 |
+
property_sheet = PropertySheet(
|
| 146 |
+
value=initial_property_from_meta_config,
|
| 147 |
+
label="Image Settings",
|
| 148 |
+
width=400,
|
| 149 |
+
height=550,
|
| 150 |
+
visible=True,
|
| 151 |
+
root_label="General"
|
| 152 |
+
)
|
| 153 |
+
gr.Markdown("## Metadata Editor")
|
| 154 |
+
with gr.Row():
|
| 155 |
+
save_button = gr.Button("Add Metadata and Save Image")
|
| 156 |
+
saved_file_output = gr.File(label="Download Image")
|
| 157 |
+
|
| 158 |
+
with gr.Row():
|
| 159 |
+
gr.Examples(
|
| 160 |
+
examples=[
|
| 161 |
+
["./examples/image_with_meta.png", "./examples/image_with_meta.png"]
|
| 162 |
+
],
|
| 163 |
+
fn=process_example_images,
|
| 164 |
+
inputs=[img_custom, img_all],
|
| 165 |
+
outputs=[img_custom, img_all],
|
| 166 |
+
cache_examples=True
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
input_fields = {
|
| 170 |
+
"Model": model_box,
|
| 171 |
+
"FNumber": fnumber_box,
|
| 172 |
+
"ISOSpeedRatings": iso_box,
|
| 173 |
+
"Schurn": s_churn,
|
| 174 |
+
"Description": description_box
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
output_fields = [
|
| 178 |
+
property_sheet,
|
| 179 |
+
model_box,
|
| 180 |
+
fnumber_box,
|
| 181 |
+
iso_box,
|
| 182 |
+
s_churn,
|
| 183 |
+
description_box
|
| 184 |
+
]
|
| 185 |
+
|
| 186 |
+
img_custom.load_metadata(handle_load_metadata, inputs=img_custom, outputs=output_fields)
|
| 187 |
+
img_all.load_metadata(handle_load_metadata, inputs=img_all, outputs=output_fields)
|
| 188 |
+
|
| 189 |
+
def handle_render_change(updated_config: PropertyConfig, current_state: PropertyConfig):
|
| 190 |
+
"""
|
| 191 |
+
Updates the PropertySheet state when its configuration changes.
|
| 192 |
+
|
| 193 |
+
Args:
|
| 194 |
+
updated_config: The new PropertyConfig instance from the PropertySheet.
|
| 195 |
+
current_state: The current PropertyConfig state.
|
| 196 |
+
|
| 197 |
+
Returns:
|
| 198 |
+
A tuple of (updated_config, updated_config) or (current_state, current_state) if updated_config is None.
|
| 199 |
+
"""
|
| 200 |
+
if updated_config is None:
|
| 201 |
+
return current_state, current_state
|
| 202 |
+
return updated_config, updated_config
|
| 203 |
+
|
| 204 |
+
property_sheet.change(
|
| 205 |
+
fn=handle_render_change,
|
| 206 |
+
inputs=[property_sheet, property_sheet_state],
|
| 207 |
+
outputs=[property_sheet, property_sheet_state]
|
| 208 |
+
)
|
| 209 |
+
save_button.click(
|
| 210 |
+
save_image_with_metadata,
|
| 211 |
+
inputs=[img_custom, *input_fields.values()],
|
| 212 |
+
outputs=[saved_file_output]
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
if __name__ == "__main__":
|
| 216 |
+
demo.launch()
|
src/demo/css.css
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
html {
|
| 2 |
+
font-family: Inter;
|
| 3 |
+
font-size: 16px;
|
| 4 |
+
font-weight: 400;
|
| 5 |
+
line-height: 1.5;
|
| 6 |
+
-webkit-text-size-adjust: 100%;
|
| 7 |
+
background: #fff;
|
| 8 |
+
color: #323232;
|
| 9 |
+
-webkit-font-smoothing: antialiased;
|
| 10 |
+
-moz-osx-font-smoothing: grayscale;
|
| 11 |
+
text-rendering: optimizeLegibility;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
:root {
|
| 15 |
+
--space: 1;
|
| 16 |
+
--vspace: calc(var(--space) * 1rem);
|
| 17 |
+
--vspace-0: calc(3 * var(--space) * 1rem);
|
| 18 |
+
--vspace-1: calc(2 * var(--space) * 1rem);
|
| 19 |
+
--vspace-2: calc(1.5 * var(--space) * 1rem);
|
| 20 |
+
--vspace-3: calc(0.5 * var(--space) * 1rem);
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.app {
|
| 24 |
+
max-width: 748px !important;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.prose p {
|
| 28 |
+
margin: var(--vspace) 0;
|
| 29 |
+
line-height: var(--vspace * 2);
|
| 30 |
+
font-size: 1rem;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
code {
|
| 34 |
+
font-family: "Inconsolata", sans-serif;
|
| 35 |
+
font-size: 16px;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
h1,
|
| 39 |
+
h1 code {
|
| 40 |
+
font-weight: 400;
|
| 41 |
+
line-height: calc(2.5 / var(--space) * var(--vspace));
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
h1 code {
|
| 45 |
+
background: none;
|
| 46 |
+
border: none;
|
| 47 |
+
letter-spacing: 0.05em;
|
| 48 |
+
padding-bottom: 5px;
|
| 49 |
+
position: relative;
|
| 50 |
+
padding: 0;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
h2 {
|
| 54 |
+
margin: var(--vspace-1) 0 var(--vspace-2) 0;
|
| 55 |
+
line-height: 1em;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
h3,
|
| 59 |
+
h3 code {
|
| 60 |
+
margin: var(--vspace-1) 0 var(--vspace-2) 0;
|
| 61 |
+
line-height: 1em;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
h4,
|
| 65 |
+
h5,
|
| 66 |
+
h6 {
|
| 67 |
+
margin: var(--vspace-3) 0 var(--vspace-3) 0;
|
| 68 |
+
line-height: var(--vspace);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.bigtitle,
|
| 72 |
+
h1,
|
| 73 |
+
h1 code {
|
| 74 |
+
font-size: calc(8px * 4.5);
|
| 75 |
+
word-break: break-word;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.title,
|
| 79 |
+
h2,
|
| 80 |
+
h2 code {
|
| 81 |
+
font-size: calc(8px * 3.375);
|
| 82 |
+
font-weight: lighter;
|
| 83 |
+
word-break: break-word;
|
| 84 |
+
border: none;
|
| 85 |
+
background: none;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.subheading1,
|
| 89 |
+
h3,
|
| 90 |
+
h3 code {
|
| 91 |
+
font-size: calc(8px * 1.8);
|
| 92 |
+
font-weight: 600;
|
| 93 |
+
border: none;
|
| 94 |
+
background: none;
|
| 95 |
+
letter-spacing: 0.1em;
|
| 96 |
+
text-transform: uppercase;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
h2 code {
|
| 100 |
+
padding: 0;
|
| 101 |
+
position: relative;
|
| 102 |
+
letter-spacing: 0.05em;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
blockquote {
|
| 106 |
+
font-size: calc(8px * 1.1667);
|
| 107 |
+
font-style: italic;
|
| 108 |
+
line-height: calc(1.1667 * var(--vspace));
|
| 109 |
+
margin: var(--vspace-2) var(--vspace-2);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.subheading2,
|
| 113 |
+
h4 {
|
| 114 |
+
font-size: calc(8px * 1.4292);
|
| 115 |
+
text-transform: uppercase;
|
| 116 |
+
font-weight: 600;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.subheading3,
|
| 120 |
+
h5 {
|
| 121 |
+
font-size: calc(8px * 1.2917);
|
| 122 |
+
line-height: calc(1.2917 * var(--vspace));
|
| 123 |
+
|
| 124 |
+
font-weight: lighter;
|
| 125 |
+
text-transform: uppercase;
|
| 126 |
+
letter-spacing: 0.15em;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
h6 {
|
| 130 |
+
font-size: calc(8px * 1.1667);
|
| 131 |
+
font-size: 1.1667em;
|
| 132 |
+
font-weight: normal;
|
| 133 |
+
font-style: italic;
|
| 134 |
+
font-family: "le-monde-livre-classic-byol", serif !important;
|
| 135 |
+
letter-spacing: 0px !important;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
#start .md > *:first-child {
|
| 139 |
+
margin-top: 0;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
h2 + h3 {
|
| 143 |
+
margin-top: 0;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.md hr {
|
| 147 |
+
border: none;
|
| 148 |
+
border-top: 1px solid var(--block-border-color);
|
| 149 |
+
margin: var(--vspace-2) 0 var(--vspace-2) 0;
|
| 150 |
+
}
|
| 151 |
+
.prose ul {
|
| 152 |
+
margin: var(--vspace-2) 0 var(--vspace-1) 0;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.gap {
|
| 156 |
+
gap: 0;
|
| 157 |
+
}
|
src/demo/requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio_imagemeta
|
| 2 |
+
gradio_propertysheet
|
src/demo/space.py
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import gradio as gr
|
| 3 |
+
from app import demo as app
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
_docs = {'ImageMeta': {'description': "A Gradio component for uploading or displaying images, with support for metadata extraction and a custom load_metadata event.\n\nThis component allows users to upload images (as input) or display images (as output). It includes a custom event for loading metadata, triggered by a 'Load Metadata' button in the UI, which expects ImageMetaData as input.", 'members': {'__init__': {'value': {'type': 'str | Image.Image | np.ndarray | Callable | None', 'default': 'None', 'description': 'A PIL Image, numpy array, path or URL for the default value that Image component is going to take. If a function is provided, the function will be called each time the app loads to set the initial value of this component.'}, 'format': {'type': 'str', 'default': '"webp"', 'description': 'File format (e.g. "png" or "gif"). Used to save image if it does not already have a valid format (e.g. if the image is being returned to the frontend as a numpy array or PIL Image). The format should be supported by the PIL library. Applies both when this component is used as an input or output. This parameter has no effect on SVG files.'}, 'height': {'type': 'int | str | None', 'default': 'None', 'description': 'The height of the component, specified in pixels if a number is passed, or in CSS units if a string is passed. This has no effect on the preprocessed image file or numpy array, but will affect the displayed image.'}, 'width': {'type': 'int | str | None', 'default': 'None', 'description': 'The width of the component, specified in pixels if a number is passed, or in CSS units if a string is passed. This has no effect on the preprocessed image file or numpy array, but will affect the displayed image.'}, 'image_mode': {'type': 'Literal[\n "1",\n "L",\n "P",\n "RGB",\n "RGBA",\n "CMYK",\n "YCbCr",\n "LAB",\n "HSV",\n "I",\n "F",\n ]\n | None', 'default': '"RGB"', 'description': 'The pixel format and color depth that the image should be loaded and preprocessed as. "RGB" will load the image as a color image, or "L" as black-and-white. See https://pillow.readthedocs.io/en/stable/handbook/concepts.html for other supported image modes and their meaning. This parameter has no effect on SVG or GIF files. If set to None, the image_mode will be inferred from the image file type (e.g. "RGBA" for a .png image, "RGB" in most other cases).'}, 'type': {'type': 'Literal["numpy", "pil", "filepath"]', 'default': '"numpy"', 'description': 'The format the image is converted before being passed into the prediction function. "numpy" converts the image to a numpy array with shape (height, width, 3) and values from 0 to 255, "pil" converts the image to a PIL image object, "filepath" passes a str path to a temporary file containing the image. To support animated GIFs in input, the `type` should be set to "filepath" or "pil". To support SVGs, the `type` should be set to "filepath".'}, 'label': {'type': 'str | I18nData | None', 'default': 'None', 'description': 'The label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.'}, 'every': {'type': 'Timer | float | None', 'default': 'None', 'description': 'Continously calls `value` to recalculate it if `value` is a function (has no effect otherwise). Can provide a Timer whose tick resets `value`, or a float that provides the regular interval for the reset Timer.'}, 'inputs': {'type': 'Component | Sequence[Component] | set[Component] | None', 'default': 'None', 'description': 'Components that are used as inputs to calculate `value` if `value` is a function (has no effect otherwise). `value` is recalculated any time the inputs change.'}, 'show_label': {'type': 'bool | None', 'default': 'None', 'description': 'If True, will display label.'}, 'show_download_button': {'type': 'bool', 'default': 'True', 'description': 'If True, will display button to download image. Only applies if interactive is False (e.g. if the component is used as an output).'}, 'container': {'type': 'bool', 'default': 'True', 'description': 'If True, will place the component in a container - providing some extra padding around the border.'}, 'scale': {'type': 'int | None', 'default': 'None', 'description': 'Relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.'}, 'min_width': {'type': 'int', 'default': '160', 'description': 'Minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.'}, 'interactive': {'type': 'bool | None', 'default': 'None', 'description': 'If True, will allow users to upload and edit an image; if False, can only be used to display images. If not provided, this is inferred based on whether the component is used as an input or output.'}, 'visible': {'type': 'bool', 'default': 'True', 'description': 'If False, component will be hidden.'}, 'elem_id': {'type': 'str | None', 'default': 'None', 'description': 'An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.'}, 'elem_classes': {'type': 'list[str] | str | None', 'default': 'None', 'description': 'An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.'}, 'render': {'type': 'bool', 'default': 'True', 'description': 'If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.'}, 'key': {'type': 'int | str | tuple[int | str, ...] | None', 'default': 'None', 'description': "In a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render."}, 'preserved_by_key': {'type': 'list[str] | str | None', 'default': '"value"', 'description': "A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor."}, 'show_share_button': {'type': 'bool | None', 'default': 'None', 'description': 'If True, will show a share icon in the corner of the component that allows user to share outputs to Hugging Face Spaces Discussions. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.'}, 'placeholder': {'type': 'str | None', 'default': 'None', 'description': 'Custom text for the upload area. Overrides default upload messages when provided. Accepts new lines and `#` to designate a heading.'}, 'show_fullscreen_button': {'type': 'bool', 'default': 'True', 'description': 'If True, will show a fullscreen icon in the corner of the component that allows user to view the image in fullscreen mode. If False, icon does not appear.'}, 'only_custom_metadata': {'type': 'bool', 'default': 'True', 'description': 'If True, extracts only custom metadata, excluding technical metadata like ImageWidth or ImageHeight. Defaults to True.'}, 'disable_preprocess': {'type': 'bool', 'default': 'True', 'description': 'If True, skips preprocessing and returns the raw ImageMetaData payload. Defaults to True.'}, 'popup_metadata_width': {'type': 'int | str', 'default': '400', 'description': 'Metadata popup width in pixels or CSS units. Defaults to 400.'}, 'popup_metadata_height': {'type': 'int | str', 'default': '300', 'description': 'Metadata popup height in pixels or CSS units. Defaults to 300.'}}, 'postprocess': {'value': {'type': 'numpy.ndarray | PIL.Image.Image | str | pathlib.Path | None', 'description': 'Input image as a NumPy array, PIL Image, string (file path or URL), Path object, or None.'}}, 'preprocess': {'return': {'type': 'numpy.ndarray | PIL.Image.Image | str | ImageMetaData | None', 'description': 'Preprocessed image as a NumPy array, PIL Image, filepath, ImageMetaData, or None.'}, 'value': None}}, 'events': {'clear': {'type': None, 'default': None, 'description': 'This listener is triggered when the user clears the ImageMeta using the clear button for the component.'}, 'change': {'type': None, 'default': None, 'description': 'Triggered when the value of the ImageMeta changes either because of user input (e.g. a user types in a textbox) OR because of a function update (e.g. an image receives a value from the output of an event trigger). See `.input()` for a listener that is only triggered by user input.'}, 'select': {'type': None, 'default': None, 'description': 'Event listener for when the user selects or deselects the ImageMeta. Uses event data gradio.SelectData to carry `value` referring to the label of the ImageMeta, and `selected` to refer to state of the ImageMeta. See EventData documentation on how to use this event data'}, 'upload': {'type': None, 'default': None, 'description': 'This listener is triggered when the user uploads a file into the ImageMeta.'}, 'input': {'type': None, 'default': None, 'description': 'This listener is triggered when the user changes the value of the ImageMeta.'}, 'load_metadata': {'type': None, 'default': None, 'description': "Triggered when the user clicks the 'Load Metadata' button, expecting ImageMetaData as input."}}}, '__meta__': {'additional_interfaces': {'ImageMetaData': {'source': 'class ImageMetaData(ImageData):\n pass'}}, 'user_fn_refs': {'ImageMeta': ['ImageMetaData']}}}
|
| 7 |
+
|
| 8 |
+
abs_path = os.path.join(os.path.dirname(__file__), "css.css")
|
| 9 |
+
|
| 10 |
+
with gr.Blocks(
|
| 11 |
+
css=abs_path,
|
| 12 |
+
theme=gr.themes.Default(
|
| 13 |
+
font_mono=[
|
| 14 |
+
gr.themes.GoogleFont("Inconsolata"),
|
| 15 |
+
"monospace",
|
| 16 |
+
],
|
| 17 |
+
),
|
| 18 |
+
) as demo:
|
| 19 |
+
gr.Markdown(
|
| 20 |
+
"""
|
| 21 |
+
# `gradio_imagemeta`
|
| 22 |
+
|
| 23 |
+
<div style="display: flex; gap: 7px;">
|
| 24 |
+
<img alt="Static Badge" src="https://img.shields.io/badge/version%20-%200.0.1%20-%20orange">
|
| 25 |
+
</div>
|
| 26 |
+
|
| 27 |
+
Image Preview with Metadata for Gradio Interface
|
| 28 |
+
""", elem_classes=["md-custom"], header_links=True)
|
| 29 |
+
app.render()
|
| 30 |
+
gr.Markdown(
|
| 31 |
+
"""
|
| 32 |
+
## Installation
|
| 33 |
+
|
| 34 |
+
```bash
|
| 35 |
+
pip install gradio_imagemeta
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
## Usage
|
| 39 |
+
|
| 40 |
+
```python
|
| 41 |
+
from dataclasses import dataclass, field
|
| 42 |
+
from typing import List, Any
|
| 43 |
+
import gradio as gr
|
| 44 |
+
from gradio_imagemeta import ImageMeta
|
| 45 |
+
from gradio_imagemeta.helpers import extract_metadata, add_metadata, transfer_metadata
|
| 46 |
+
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 |
+
|
| 53 |
+
@dataclass
|
| 54 |
+
class ImageSettings:
|
| 55 |
+
\"\"\"Configuration for image metadata settings.\"\"\"
|
| 56 |
+
model: str = field(default="", metadata={"label": "Model"})
|
| 57 |
+
f_number: str = field(default="", metadata={"label": "FNumber"})
|
| 58 |
+
iso_speed_ratings: str = field(default="", metadata={"label": "ISOSpeedRatings"})
|
| 59 |
+
s_churn: float = field(
|
| 60 |
+
default=0.0,
|
| 61 |
+
metadata={"component": "slider", "label": "Schurn", "minimum": 0.0, "maximum": 1.0, "step": 0.01},
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
@dataclass
|
| 65 |
+
class PropertyConfig:
|
| 66 |
+
\"\"\"Root configuration for image properties, including nested image settings.\"\"\"
|
| 67 |
+
image_settings: ImageSettings = field(default_factory=ImageSettings)
|
| 68 |
+
description: str = field(default="", metadata={"label": "Description"})
|
| 69 |
+
|
| 70 |
+
def process_example_images(img_custom_path: str, img_all_path: str) -> tuple[str, str]:
|
| 71 |
+
\"\"\"
|
| 72 |
+
Processes example image paths for display in ImageMeta components.
|
| 73 |
+
|
| 74 |
+
Args:
|
| 75 |
+
img_custom_path: File path for the image to display in img_custom.
|
| 76 |
+
img_all_path: File path for the image to display in img_all.
|
| 77 |
+
|
| 78 |
+
Returns:
|
| 79 |
+
Tuple of file paths for img_custom and img_all outputs.
|
| 80 |
+
\"\"\"
|
| 81 |
+
# Verify file existence
|
| 82 |
+
if not Path(img_custom_path).is_file():
|
| 83 |
+
raise FileNotFoundError(f"Image not found: {img_custom_path}")
|
| 84 |
+
if not Path(img_all_path).is_file():
|
| 85 |
+
raise FileNotFoundError(f"Image not found: {img_all_path}")
|
| 86 |
+
|
| 87 |
+
# Return file paths as strings (ImageMeta accepts file paths as input)
|
| 88 |
+
return str(img_custom_path), str(img_all_path)
|
| 89 |
+
|
| 90 |
+
def handle_load_metadata(image_data: ImageMeta | None) -> List[Any]:
|
| 91 |
+
\"\"\"
|
| 92 |
+
Processes image metadata and maps it to output components.
|
| 93 |
+
|
| 94 |
+
Args:
|
| 95 |
+
image_data: ImageMeta object containing image data and metadata, or None.
|
| 96 |
+
|
| 97 |
+
Returns:
|
| 98 |
+
A list of values for output components (Textbox, Slider, or PropertySheet instances).
|
| 99 |
+
\"\"\"
|
| 100 |
+
if not image_data:
|
| 101 |
+
return [gr.Textbox(value="") for _ in output_fields]
|
| 102 |
+
|
| 103 |
+
metadata = extract_metadata(image_data, only_custom_metadata=True)
|
| 104 |
+
dataclass_fields = build_dataclass_fields(PropertyConfig)
|
| 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.Textbox(value=value)
|
| 113 |
+
|
| 114 |
+
return output_values
|
| 115 |
+
|
| 116 |
+
def save_image_with_metadata(image_data: Any, *inputs: Any) -> str | None:
|
| 117 |
+
\"\"\"
|
| 118 |
+
Saves an image with updated metadata to a file.
|
| 119 |
+
|
| 120 |
+
Args:
|
| 121 |
+
image_data: Input image data (e.g., file path or PIL Image).
|
| 122 |
+
*inputs: Variable number of input values from UI components (Textbox, Slider).
|
| 123 |
+
|
| 124 |
+
Returns:
|
| 125 |
+
The file path of the saved image, or None if no image is provided.
|
| 126 |
+
\"\"\"
|
| 127 |
+
if not image_data:
|
| 128 |
+
return 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 dataclass_fields.keys()}
|
| 134 |
+
|
| 135 |
+
new_filepath = output_dir / "image_with_meta.png"
|
| 136 |
+
add_metadata(image_data, metadata, new_filepath)
|
| 137 |
+
|
| 138 |
+
return str(new_filepath)
|
| 139 |
+
|
| 140 |
+
initial_property_from_meta_config = PropertyConfig()
|
| 141 |
+
|
| 142 |
+
with gr.Blocks() as demo:
|
| 143 |
+
gr.Markdown("# ImageMeta Component Demo")
|
| 144 |
+
gr.Markdown(
|
| 145 |
+
\"\"\"
|
| 146 |
+
**To Test:**
|
| 147 |
+
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.
|
| 148 |
+
2. Click the 'Info' icon (ⓘ) in the top-left of the image component to view the metadata panel.
|
| 149 |
+
3. Click 'Load Metadata' in the popup to populate the fields below with metadata values (`Model`, `FNumber`, `ISOSpeedRatings`, `Schurn`, `Description`).
|
| 150 |
+
4. The section below displays how metadata is rendered in components and the `PropertySheet` custom component, showing the hierarchical structure of the image settings.
|
| 151 |
+
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.
|
| 152 |
+
\"\"\"
|
| 153 |
+
)
|
| 154 |
+
property_sheet_state = gr.State(value=initial_property_from_meta_config)
|
| 155 |
+
with gr.Row():
|
| 156 |
+
img_custom = ImageMeta(
|
| 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")
|
| 174 |
+
gr.Markdown("### Individual Components")
|
| 175 |
+
with gr.Row():
|
| 176 |
+
model_box = gr.Textbox(label="Model")
|
| 177 |
+
fnumber_box = gr.Textbox(label="FNumber")
|
| 178 |
+
iso_box = gr.Textbox(label="ISOSpeedRatings")
|
| 179 |
+
s_churn = gr.Slider(label="Schurn", value=1.0, minimum=0.0, maximum=1.0, step=0.1)
|
| 180 |
+
description_box = gr.Textbox(label="Description")
|
| 181 |
+
|
| 182 |
+
gr.Markdown("### PropertySheet Component")
|
| 183 |
+
with gr.Row():
|
| 184 |
+
property_sheet = PropertySheet(
|
| 185 |
+
value=initial_property_from_meta_config,
|
| 186 |
+
label="Image Settings",
|
| 187 |
+
width=400,
|
| 188 |
+
height=550,
|
| 189 |
+
visible=True,
|
| 190 |
+
root_label="General"
|
| 191 |
+
)
|
| 192 |
+
gr.Markdown("## Metadata Editor")
|
| 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,
|
| 210 |
+
"FNumber": fnumber_box,
|
| 211 |
+
"ISOSpeedRatings": iso_box,
|
| 212 |
+
"Schurn": s_churn,
|
| 213 |
+
"Description": description_box
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
output_fields = [
|
| 217 |
+
property_sheet,
|
| 218 |
+
model_box,
|
| 219 |
+
fnumber_box,
|
| 220 |
+
iso_box,
|
| 221 |
+
s_churn,
|
| 222 |
+
description_box
|
| 223 |
+
]
|
| 224 |
+
|
| 225 |
+
img_custom.load_metadata(handle_load_metadata, inputs=img_custom, outputs=output_fields)
|
| 226 |
+
img_all.load_metadata(handle_load_metadata, inputs=img_all, outputs=output_fields)
|
| 227 |
+
|
| 228 |
+
def handle_render_change(updated_config: PropertyConfig, current_state: PropertyConfig):
|
| 229 |
+
\"\"\"
|
| 230 |
+
Updates the PropertySheet state when its configuration changes.
|
| 231 |
+
|
| 232 |
+
Args:
|
| 233 |
+
updated_config: The new PropertyConfig instance from the PropertySheet.
|
| 234 |
+
current_state: The current PropertyConfig state.
|
| 235 |
+
|
| 236 |
+
Returns:
|
| 237 |
+
A tuple of (updated_config, updated_config) or (current_state, current_state) if updated_config is None.
|
| 238 |
+
\"\"\"
|
| 239 |
+
if updated_config is None:
|
| 240 |
+
return current_state, current_state
|
| 241 |
+
return updated_config, updated_config
|
| 242 |
+
|
| 243 |
+
property_sheet.change(
|
| 244 |
+
fn=handle_render_change,
|
| 245 |
+
inputs=[property_sheet, property_sheet_state],
|
| 246 |
+
outputs=[property_sheet, property_sheet_state]
|
| 247 |
+
)
|
| 248 |
+
save_button.click(
|
| 249 |
+
save_image_with_metadata,
|
| 250 |
+
inputs=[img_custom, *input_fields.values()],
|
| 251 |
+
outputs=[saved_file_output]
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
if __name__ == "__main__":
|
| 255 |
+
demo.launch()
|
| 256 |
+
```
|
| 257 |
+
""", elem_classes=["md-custom"], header_links=True)
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
gr.Markdown("""
|
| 261 |
+
## `ImageMeta`
|
| 262 |
+
|
| 263 |
+
### Initialization
|
| 264 |
+
""", elem_classes=["md-custom"], header_links=True)
|
| 265 |
+
|
| 266 |
+
gr.ParamViewer(value=_docs["ImageMeta"]["members"]["__init__"], linkify=['ImageMetaData'])
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
gr.Markdown("### Events")
|
| 270 |
+
gr.ParamViewer(value=_docs["ImageMeta"]["events"], linkify=['Event'])
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
gr.Markdown("""
|
| 276 |
+
|
| 277 |
+
### User function
|
| 278 |
+
|
| 279 |
+
The impact on the users predict function varies depending on whether the component is used as an input or output for an event (or both).
|
| 280 |
+
|
| 281 |
+
- When used as an Input, the component only impacts the input signature of the user function.
|
| 282 |
+
- When used as an output, the component only impacts the return signature of the user function.
|
| 283 |
+
|
| 284 |
+
The code snippet below is accurate in cases where the component is used as both an input and an output.
|
| 285 |
+
|
| 286 |
+
- **As input:** Is passed, preprocessed image as a NumPy array, PIL Image, filepath, ImageMetaData, or None.
|
| 287 |
+
- **As output:** Should return, input image as a NumPy array, PIL Image, string (file path or URL), Path object, or None.
|
| 288 |
+
|
| 289 |
+
```python
|
| 290 |
+
def predict(
|
| 291 |
+
value: numpy.ndarray | PIL.Image.Image | str | ImageMetaData | None
|
| 292 |
+
) -> numpy.ndarray | PIL.Image.Image | str | pathlib.Path | None:
|
| 293 |
+
return value
|
| 294 |
+
```
|
| 295 |
+
""", elem_classes=["md-custom", "ImageMeta-user-fn"], header_links=True)
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
code_ImageMetaData = gr.Markdown("""
|
| 301 |
+
## `ImageMetaData`
|
| 302 |
+
```python
|
| 303 |
+
class ImageMetaData(ImageData):
|
| 304 |
+
pass
|
| 305 |
+
```""", elem_classes=["md-custom", "ImageMetaData"], header_links=True)
|
| 306 |
+
|
| 307 |
+
demo.load(None, js=r"""function() {
|
| 308 |
+
const refs = {
|
| 309 |
+
ImageMetaData: [], };
|
| 310 |
+
const user_fn_refs = {
|
| 311 |
+
ImageMeta: ['ImageMetaData'], };
|
| 312 |
+
requestAnimationFrame(() => {
|
| 313 |
+
|
| 314 |
+
Object.entries(user_fn_refs).forEach(([key, refs]) => {
|
| 315 |
+
if (refs.length > 0) {
|
| 316 |
+
const el = document.querySelector(`.${key}-user-fn`);
|
| 317 |
+
if (!el) return;
|
| 318 |
+
refs.forEach(ref => {
|
| 319 |
+
el.innerHTML = el.innerHTML.replace(
|
| 320 |
+
new RegExp("\\b"+ref+"\\b", "g"),
|
| 321 |
+
`<a href="#h-${ref.toLowerCase()}">${ref}</a>`
|
| 322 |
+
);
|
| 323 |
+
})
|
| 324 |
+
}
|
| 325 |
+
})
|
| 326 |
+
|
| 327 |
+
Object.entries(refs).forEach(([key, refs]) => {
|
| 328 |
+
if (refs.length > 0) {
|
| 329 |
+
const el = document.querySelector(`.${key}`);
|
| 330 |
+
if (!el) return;
|
| 331 |
+
refs.forEach(ref => {
|
| 332 |
+
el.innerHTML = el.innerHTML.replace(
|
| 333 |
+
new RegExp("\\b"+ref+"\\b", "g"),
|
| 334 |
+
`<a href="#h-${ref.toLowerCase()}">${ref}</a>`
|
| 335 |
+
);
|
| 336 |
+
})
|
| 337 |
+
}
|
| 338 |
+
})
|
| 339 |
+
})
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
""")
|
| 343 |
+
|
| 344 |
+
demo.launch()
|
src/examples/image_with_meta.png
ADDED
|
Git LFS Details
|
src/frontend/Example.svelte
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import Image from "./shared/Image.svelte";
|
| 3 |
+
import type { FileData } from "@gradio/client";
|
| 4 |
+
|
| 5 |
+
export let value: null | FileData;
|
| 6 |
+
export let type: "gallery" | "table";
|
| 7 |
+
export let selected = false;
|
| 8 |
+
</script>
|
| 9 |
+
|
| 10 |
+
<div
|
| 11 |
+
class="container"
|
| 12 |
+
class:table={type === "table"}
|
| 13 |
+
class:gallery={type === "gallery"}
|
| 14 |
+
class:selected
|
| 15 |
+
class:border={value}
|
| 16 |
+
>
|
| 17 |
+
{#if value}
|
| 18 |
+
<Image src={value.url} alt="" />
|
| 19 |
+
{/if}
|
| 20 |
+
</div>
|
| 21 |
+
|
| 22 |
+
<style>
|
| 23 |
+
.container :global(img) {
|
| 24 |
+
width: 100%;
|
| 25 |
+
height: 100%;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
.container.selected {
|
| 29 |
+
border-color: var(--border-color-accent);
|
| 30 |
+
}
|
| 31 |
+
.border.table {
|
| 32 |
+
border: 2px solid var(--border-color-primary);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.container.table {
|
| 36 |
+
margin: 0 auto;
|
| 37 |
+
border-radius: var(--radius-lg);
|
| 38 |
+
overflow: hidden;
|
| 39 |
+
width: var(--size-20);
|
| 40 |
+
height: var(--size-20);
|
| 41 |
+
object-fit: cover;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.container.gallery {
|
| 45 |
+
width: var(--size-20);
|
| 46 |
+
max-width: var(--size-20);
|
| 47 |
+
object-fit: cover;
|
| 48 |
+
}
|
| 49 |
+
</style>
|
src/frontend/Index.svelte
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<svelte:options accessors={true} />
|
| 2 |
+
|
| 3 |
+
<script context="module" lang="ts">
|
| 4 |
+
/**
|
| 5 |
+
* Exports base components for reuse.
|
| 6 |
+
*/
|
| 7 |
+
export { default as BaseImageUploader } from "./shared/ImageUploader.svelte";
|
| 8 |
+
export { default as BaseStaticImage } from "./shared/ImagePreview.svelte";
|
| 9 |
+
export { default as BaseExample } from "./Example.svelte";
|
| 10 |
+
export { default as BaseImage } from "./shared/Image.svelte";
|
| 11 |
+
</script>
|
| 12 |
+
|
| 13 |
+
<script lang="ts">
|
| 14 |
+
import type { Gradio, SelectData, ShareData } from "@gradio/utils";
|
| 15 |
+
import StaticImage from "./shared/ImagePreview.svelte";
|
| 16 |
+
import ImageUploader from "./shared/ImageUploader.svelte";
|
| 17 |
+
import { afterUpdate } from "svelte";
|
| 18 |
+
import { Block, Empty, UploadText } from "@gradio/atoms";
|
| 19 |
+
import { Image } from "@gradio/icons";
|
| 20 |
+
import { StatusTracker } from "@gradio/statustracker";
|
| 21 |
+
import { upload, type FileData } from "@gradio/client";
|
| 22 |
+
import type { LoadingStatus } from "@gradio/statustracker";
|
| 23 |
+
|
| 24 |
+
type sources = "upload";
|
| 25 |
+
|
| 26 |
+
/**
|
| 27 |
+
* Props for the Index component.
|
| 28 |
+
*/
|
| 29 |
+
export let value_is_output: boolean = false; // Whether the component is used as an output.
|
| 30 |
+
export let elem_id: string = ""; // HTML element ID for the component.
|
| 31 |
+
export let elem_classes: string[] = []; // HTML element classes for the component.
|
| 32 |
+
export let visible: boolean = true; // Whether the component is visible.
|
| 33 |
+
export let value: null | FileData = null; // Image data with URL and optional metadata.
|
| 34 |
+
export let label: string; // Label displayed above the component.
|
| 35 |
+
export let show_label: boolean; // Whether to display the label.
|
| 36 |
+
export let show_download_button: boolean; // Whether to show the download button (for StaticImage).
|
| 37 |
+
export let root: string; // Root URL for file uploads.
|
| 38 |
+
export let only_custom_metadata: boolean = true; // Whether to filter out technical metadata.
|
| 39 |
+
export let popup_metadata_width: number | string = 400; // Width of the metadata popup (pixels or CSS units).
|
| 40 |
+
export let popup_metadata_height: number | string = 300; // Height of the metadata popup (pixels or CSS units).
|
| 41 |
+
export let height: number | undefined; // Component height (pixels).
|
| 42 |
+
export let width: number | undefined; // Component width (pixels).
|
| 43 |
+
export let _selectable: boolean = false; // Whether the image is clickable for coordinate selection.
|
| 44 |
+
export let container: boolean = true; // Whether to wrap the component in a container.
|
| 45 |
+
export let scale: number | null = null; // Relative size compared to adjacent components.
|
| 46 |
+
export let min_width: number | undefined = undefined; // Minimum width in pixels.
|
| 47 |
+
export let loading_status: LoadingStatus; // Status of loading operations (e.g., upload, error).
|
| 48 |
+
export let show_share_button: boolean = false; // Whether to show the share button (for StaticImage).
|
| 49 |
+
export let sources: sources[] = ["upload"]; // Input sources (only "upload" supported).
|
| 50 |
+
export let interactive: boolean; // Whether the component allows image uploads.
|
| 51 |
+
export let pending: boolean; // Whether an operation is pending.
|
| 52 |
+
export let placeholder: string | undefined = undefined; // Placeholder text for the upload area.
|
| 53 |
+
export let show_fullscreen_button: boolean; // Whether to show the fullscreen button.
|
| 54 |
+
export let gradio: Gradio<{
|
| 55 |
+
input: never;
|
| 56 |
+
change: never;
|
| 57 |
+
error: string;
|
| 58 |
+
edit: never;
|
| 59 |
+
drag: never;
|
| 60 |
+
upload: never;
|
| 61 |
+
clear: never;
|
| 62 |
+
select: SelectData;
|
| 63 |
+
share: ShareData;
|
| 64 |
+
clear_status: LoadingStatus;
|
| 65 |
+
load_metadata: never;
|
| 66 |
+
}>; // Gradio interface for event dispatching.
|
| 67 |
+
|
| 68 |
+
let old_value: null | FileData = null; // Previous value for change detection.
|
| 69 |
+
let fullscreen: boolean = false; // Whether the image is in fullscreen mode.
|
| 70 |
+
let uploading: boolean = false; // Whether an upload is in progress.
|
| 71 |
+
$: input_ready = !uploading; // Whether the input is ready (not uploading).
|
| 72 |
+
let dragging: boolean; // Whether a file is being dragged over the component.
|
| 73 |
+
let active_source: sources = "upload"; // Current input source (only "upload" supported).
|
| 74 |
+
let upload_component: ImageUploader; // Reference to the ImageUploader component.
|
| 75 |
+
|
| 76 |
+
// Reactive: Detects value changes and dispatches events.
|
| 77 |
+
$: {
|
| 78 |
+
if (JSON.stringify(value) !== JSON.stringify(old_value)) {
|
| 79 |
+
old_value = value;
|
| 80 |
+
gradio.dispatch("change");
|
| 81 |
+
if (!value_is_output) {
|
| 82 |
+
gradio.dispatch("input");
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
/**
|
| 88 |
+
* Resets value_is_output after component updates.
|
| 89 |
+
*/
|
| 90 |
+
afterUpdate(() => {
|
| 91 |
+
value_is_output = false;
|
| 92 |
+
});
|
| 93 |
+
|
| 94 |
+
/**
|
| 95 |
+
* Handles drag events to update dragging state.
|
| 96 |
+
* @param event - Drag event (dragenter, dragover, dragleave).
|
| 97 |
+
*/
|
| 98 |
+
const handle_drag_event = (event: Event): void => {
|
| 99 |
+
const drag_event = event as DragEvent;
|
| 100 |
+
drag_event.preventDefault();
|
| 101 |
+
drag_event.stopPropagation();
|
| 102 |
+
if (drag_event.type === "dragenter" || drag_event.type === "dragover") {
|
| 103 |
+
dragging = true;
|
| 104 |
+
} else if (drag_event.type === "dragleave") {
|
| 105 |
+
dragging = false;
|
| 106 |
+
}
|
| 107 |
+
};
|
| 108 |
+
|
| 109 |
+
/**
|
| 110 |
+
* Handles file drop to initiate upload via ImageUploader.
|
| 111 |
+
* @param event - Drag event containing dropped files.
|
| 112 |
+
*/
|
| 113 |
+
const handle_drop = (event: Event): void => {
|
| 114 |
+
if (interactive) {
|
| 115 |
+
const drop_event = event as DragEvent;
|
| 116 |
+
drop_event.preventDefault();
|
| 117 |
+
drop_event.stopPropagation();
|
| 118 |
+
dragging = false;
|
| 119 |
+
|
| 120 |
+
if (upload_component) {
|
| 121 |
+
upload_component.loadFilesFromDrop(drop_event);
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
};
|
| 125 |
+
|
| 126 |
+
/**
|
| 127 |
+
* Dispatches the load_metadata event when triggered by child components.
|
| 128 |
+
*/
|
| 129 |
+
function handle_load_metadata(): void {
|
| 130 |
+
gradio.dispatch("load_metadata");
|
| 131 |
+
}
|
| 132 |
+
</script>
|
| 133 |
+
|
| 134 |
+
{#if !interactive}
|
| 135 |
+
<Block
|
| 136 |
+
{visible}
|
| 137 |
+
variant={"solid"}
|
| 138 |
+
border_mode={dragging ? "focus" : "base"}
|
| 139 |
+
padding={false}
|
| 140 |
+
{elem_id}
|
| 141 |
+
{elem_classes}
|
| 142 |
+
height={height || undefined}
|
| 143 |
+
{width}
|
| 144 |
+
allow_overflow={false}
|
| 145 |
+
{container}
|
| 146 |
+
{scale}
|
| 147 |
+
{min_width}
|
| 148 |
+
bind:fullscreen
|
| 149 |
+
>
|
| 150 |
+
<StatusTracker
|
| 151 |
+
autoscroll={gradio.autoscroll}
|
| 152 |
+
i18n={gradio.i18n}
|
| 153 |
+
{...loading_status}
|
| 154 |
+
/>
|
| 155 |
+
<StaticImage
|
| 156 |
+
on:select={({ detail }) => gradio.dispatch("select", detail)}
|
| 157 |
+
on:share={({ detail }) => gradio.dispatch("share", detail)}
|
| 158 |
+
on:error={({ detail }) => gradio.dispatch("error", detail)}
|
| 159 |
+
on:load_metadata={handle_load_metadata}
|
| 160 |
+
on:fullscreen={({ detail }) => {
|
| 161 |
+
fullscreen = detail;
|
| 162 |
+
}}
|
| 163 |
+
{fullscreen}
|
| 164 |
+
{value}
|
| 165 |
+
{label}
|
| 166 |
+
{show_label}
|
| 167 |
+
{show_download_button}
|
| 168 |
+
selectable={_selectable}
|
| 169 |
+
{show_share_button}
|
| 170 |
+
i18n={gradio.i18n}
|
| 171 |
+
{show_fullscreen_button}
|
| 172 |
+
{only_custom_metadata}
|
| 173 |
+
{popup_metadata_width}
|
| 174 |
+
{popup_metadata_height}
|
| 175 |
+
/>
|
| 176 |
+
</Block>
|
| 177 |
+
{:else}
|
| 178 |
+
<Block
|
| 179 |
+
{visible}
|
| 180 |
+
variant={value === null ? "dashed" : "solid"}
|
| 181 |
+
border_mode={dragging ? "focus" : "base"}
|
| 182 |
+
padding={false}
|
| 183 |
+
{elem_id}
|
| 184 |
+
{elem_classes}
|
| 185 |
+
height={height || undefined}
|
| 186 |
+
{width}
|
| 187 |
+
allow_overflow={false}
|
| 188 |
+
{container}
|
| 189 |
+
{scale}
|
| 190 |
+
{min_width}
|
| 191 |
+
bind:fullscreen
|
| 192 |
+
on:dragenter={handle_drag_event}
|
| 193 |
+
on:dragleave={handle_drag_event}
|
| 194 |
+
on:dragover={handle_drag_event}
|
| 195 |
+
on:drop={handle_drop}
|
| 196 |
+
>
|
| 197 |
+
<StatusTracker
|
| 198 |
+
autoscroll={gradio.autoscroll}
|
| 199 |
+
i18n={gradio.i18n}
|
| 200 |
+
{...loading_status}
|
| 201 |
+
on:clear_status={() => gradio.dispatch("clear_status", loading_status)}
|
| 202 |
+
/>
|
| 203 |
+
|
| 204 |
+
<ImageUploader
|
| 205 |
+
bind:this={upload_component}
|
| 206 |
+
bind:uploading
|
| 207 |
+
bind:active_source
|
| 208 |
+
bind:value
|
| 209 |
+
bind:dragging
|
| 210 |
+
selectable={_selectable}
|
| 211 |
+
{root}
|
| 212 |
+
{sources}
|
| 213 |
+
{fullscreen}
|
| 214 |
+
on:edit={() => gradio.dispatch("edit")}
|
| 215 |
+
on:clear={() => {
|
| 216 |
+
gradio.dispatch("clear");
|
| 217 |
+
}}
|
| 218 |
+
on:drag={({ detail }) => (dragging = detail)}
|
| 219 |
+
on:upload={() => gradio.dispatch("upload")}
|
| 220 |
+
on:select={({ detail }) => gradio.dispatch("select", detail)}
|
| 221 |
+
on:share={({ detail }) => gradio.dispatch("share", detail)}
|
| 222 |
+
on:load_metadata={handle_load_metadata}
|
| 223 |
+
on:error={({ detail }) => {
|
| 224 |
+
loading_status = loading_status || {};
|
| 225 |
+
loading_status.status = "error";
|
| 226 |
+
gradio.dispatch("error", detail);
|
| 227 |
+
}}
|
| 228 |
+
on:fullscreen={({ detail }) => {
|
| 229 |
+
fullscreen = detail;
|
| 230 |
+
}}
|
| 231 |
+
{label}
|
| 232 |
+
{show_label}
|
| 233 |
+
{pending}
|
| 234 |
+
{show_fullscreen_button}
|
| 235 |
+
max_file_size={gradio.max_file_size}
|
| 236 |
+
i18n={gradio.i18n}
|
| 237 |
+
upload={(...args) => gradio.client.upload(...args)}
|
| 238 |
+
stream_handler={gradio.client?.stream}
|
| 239 |
+
{only_custom_metadata}
|
| 240 |
+
{popup_metadata_width}
|
| 241 |
+
{popup_metadata_height}
|
| 242 |
+
>
|
| 243 |
+
<UploadText i18n={gradio.i18n} type="image" {placeholder} />
|
| 244 |
+
</ImageUploader>
|
| 245 |
+
</Block>
|
| 246 |
+
{/if}
|
src/frontend/gradio.config.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default {
|
| 2 |
+
plugins: [],
|
| 3 |
+
svelte: {
|
| 4 |
+
preprocess: [],
|
| 5 |
+
},
|
| 6 |
+
build: {
|
| 7 |
+
target: "modules",
|
| 8 |
+
},
|
| 9 |
+
};
|
src/frontend/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
src/frontend/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "gradio_imagemeta",
|
| 3 |
+
"version": "0.22.13",
|
| 4 |
+
"description": "Gradio UI packages",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"author": "",
|
| 7 |
+
"license": "ISC",
|
| 8 |
+
"private": false,
|
| 9 |
+
"dependencies": {
|
| 10 |
+
"@gradio/atoms": "0.16.3",
|
| 11 |
+
"@gradio/client": "1.15.6",
|
| 12 |
+
"@gradio/icons": "0.12.0",
|
| 13 |
+
"@gradio/statustracker": "0.10.15",
|
| 14 |
+
"@gradio/upload": "0.16.11",
|
| 15 |
+
"@gradio/utils": "0.10.2",
|
| 16 |
+
"@gradio/wasm": "0.18.1",
|
| 17 |
+
"cropperjs": "^1.5.12",
|
| 18 |
+
"exifr": "^7.1.3",
|
| 19 |
+
"lazy-brush": "^1.0.1",
|
| 20 |
+
"resize-observer-polyfill": "^1.5.1"
|
| 21 |
+
},
|
| 22 |
+
"devDependencies": {
|
| 23 |
+
"@gradio/preview": "0.14.0"
|
| 24 |
+
},
|
| 25 |
+
"main_changeset": true,
|
| 26 |
+
"main": "./Index.svelte",
|
| 27 |
+
"exports": {
|
| 28 |
+
"./package.json": "./package.json",
|
| 29 |
+
".": {
|
| 30 |
+
"gradio": "./Index.svelte",
|
| 31 |
+
"svelte": "./dist/Index.svelte",
|
| 32 |
+
"types": "./dist/Index.svelte.d.ts"
|
| 33 |
+
},
|
| 34 |
+
"./example": {
|
| 35 |
+
"gradio": "./Example.svelte",
|
| 36 |
+
"svelte": "./dist/Example.svelte",
|
| 37 |
+
"types": "./dist/Example.svelte.d.ts"
|
| 38 |
+
},
|
| 39 |
+
"./base": {
|
| 40 |
+
"gradio": "./shared/ImagePreview.svelte",
|
| 41 |
+
"svelte": "./dist/shared/ImagePreview.svelte",
|
| 42 |
+
"types": "./dist/shared/ImagePreview.svelte.d.ts"
|
| 43 |
+
},
|
| 44 |
+
"./shared": {
|
| 45 |
+
"gradio": "./shared/index.ts",
|
| 46 |
+
"svelte": "./dist/shared/index.js",
|
| 47 |
+
"types": "./dist/shared/index.d.ts"
|
| 48 |
+
}
|
| 49 |
+
},
|
| 50 |
+
"peerDependencies": {
|
| 51 |
+
"svelte": "^4.0.0"
|
| 52 |
+
},
|
| 53 |
+
"repository": {
|
| 54 |
+
"type": "git",
|
| 55 |
+
"url": "git+https://github.com/gradio-app/gradio.git",
|
| 56 |
+
"directory": "js/image"
|
| 57 |
+
}
|
| 58 |
+
}
|
src/frontend/shared/Image.svelte
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import type { HTMLImgAttributes } from "svelte/elements";
|
| 3 |
+
|
| 4 |
+
interface Props extends HTMLImgAttributes {
|
| 5 |
+
"data-testid"?: string;
|
| 6 |
+
}
|
| 7 |
+
type $$Props = Props;
|
| 8 |
+
|
| 9 |
+
import { resolve_wasm_src } from "@gradio/wasm/svelte";
|
| 10 |
+
|
| 11 |
+
export let src: HTMLImgAttributes["src"] = undefined;
|
| 12 |
+
|
| 13 |
+
let resolved_src: typeof src;
|
| 14 |
+
|
| 15 |
+
// The `src` prop can be updated before the Promise from `resolve_wasm_src` is resolved.
|
| 16 |
+
// In such a case, the resolved value for the old `src` has to be discarded,
|
| 17 |
+
// This variable `latest_src` is used to pick up only the value resolved for the latest `src` prop.
|
| 18 |
+
let latest_src: typeof src;
|
| 19 |
+
$: {
|
| 20 |
+
// In normal (non-Wasm) Gradio, the `<img>` element should be rendered with the passed `src` props immediately
|
| 21 |
+
// without waiting for `resolve_wasm_src()` to resolve.
|
| 22 |
+
// If it waits, a blank image is displayed until the async task finishes
|
| 23 |
+
// and it leads to undesirable flickering.
|
| 24 |
+
// So set `src` to `resolved_src` here.
|
| 25 |
+
resolved_src = src;
|
| 26 |
+
|
| 27 |
+
latest_src = src;
|
| 28 |
+
const resolving_src = src;
|
| 29 |
+
resolve_wasm_src(resolving_src).then((s) => {
|
| 30 |
+
if (latest_src === resolving_src) {
|
| 31 |
+
resolved_src = s;
|
| 32 |
+
}
|
| 33 |
+
});
|
| 34 |
+
}
|
| 35 |
+
</script>
|
| 36 |
+
|
| 37 |
+
<!-- svelte-ignore a11y-missing-attribute -->
|
| 38 |
+
<img src={resolved_src} {...$$restProps} on:load />
|
| 39 |
+
|
| 40 |
+
<style>
|
| 41 |
+
img {
|
| 42 |
+
object-fit: cover;
|
| 43 |
+
}
|
| 44 |
+
</style>
|
src/frontend/shared/ImagePreview.svelte
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import { createEventDispatcher, onMount } from "svelte";
|
| 3 |
+
import type { SelectData } from "@gradio/utils";
|
| 4 |
+
import { uploadToHuggingFace } from "@gradio/utils";
|
| 5 |
+
import {
|
| 6 |
+
BlockLabel,
|
| 7 |
+
Empty,
|
| 8 |
+
IconButton,
|
| 9 |
+
ShareButton,
|
| 10 |
+
IconButtonWrapper,
|
| 11 |
+
FullscreenButton
|
| 12 |
+
} from "@gradio/atoms";
|
| 13 |
+
import { Download, Image as ImageIcon, Info } from "@gradio/icons";
|
| 14 |
+
import { get_coordinates_of_clicked_image } from "./utils";
|
| 15 |
+
import Image from "./Image.svelte";
|
| 16 |
+
import { DownloadLink } from "@gradio/wasm/svelte";
|
| 17 |
+
import * as exifr from "exifr";
|
| 18 |
+
import type { I18nFormatter } from "@gradio/utils";
|
| 19 |
+
import type { FileData } from "@gradio/client";
|
| 20 |
+
|
| 21 |
+
/**
|
| 22 |
+
* Props for the ImagePreview component.
|
| 23 |
+
*/
|
| 24 |
+
export let value: null | FileData; // Image data with URL and optional metadata.
|
| 25 |
+
export let label: string | undefined = undefined; // Label displayed above the component.
|
| 26 |
+
export let show_label: boolean; // Whether to display the label.
|
| 27 |
+
export let show_download_button: boolean = true; // Whether to show the download button.
|
| 28 |
+
export let selectable: boolean = false; // Whether the image is clickable for coordinate selection.
|
| 29 |
+
export let show_share_button: boolean = false; // Whether to show the share button.
|
| 30 |
+
export let i18n: I18nFormatter; // Formatter for internationalization.
|
| 31 |
+
export let show_fullscreen_button: boolean = true; // Whether to show the fullscreen button.
|
| 32 |
+
export let display_icon_button_wrapper_top_corner: boolean = false; // Whether to position buttons in the top corner.
|
| 33 |
+
export let fullscreen: boolean = false; // Whether the image is in fullscreen mode.
|
| 34 |
+
export let only_custom_metadata: boolean = true; // Whether to filter out technical metadata.
|
| 35 |
+
export let popup_metadata_width: number | string = 400; // Width of the metadata popup (pixels or CSS units).
|
| 36 |
+
export let popup_metadata_height: number | string = 300; // Height of the metadata popup (pixels or CSS units).
|
| 37 |
+
export let height: number | string | undefined = undefined; // Component height (pixels or CSS units).
|
| 38 |
+
export let width: number | string | undefined = undefined; // Component width (pixels or CSS units).
|
| 39 |
+
|
| 40 |
+
let metadata: Record<string, any> | null = null; // Extracted image metadata.
|
| 41 |
+
let showMetadataPopup: boolean = false; // Whether the metadata popup is visible.
|
| 42 |
+
let image_container: HTMLElement; // Reference to the image container element.
|
| 43 |
+
|
| 44 |
+
// Event dispatcher for component events.
|
| 45 |
+
const dispatch = createEventDispatcher<{
|
| 46 |
+
change: string; // Triggered when the image changes.
|
| 47 |
+
select: SelectData; // Triggered when clicking the image with coordinates.
|
| 48 |
+
fullscreen: boolean; // Triggered when toggling fullscreen mode.
|
| 49 |
+
load_metadata: never; // Triggered when loading metadata.
|
| 50 |
+
}>();
|
| 51 |
+
|
| 52 |
+
// List of technical metadata keys to filter out when only_custom_metadata is true.
|
| 53 |
+
const technicalMetadata: string[] = [
|
| 54 |
+
"ImageWidth",
|
| 55 |
+
"ImageHeight",
|
| 56 |
+
"BitDepth",
|
| 57 |
+
"ColorType",
|
| 58 |
+
"Compression",
|
| 59 |
+
"Filter",
|
| 60 |
+
"Interlace"
|
| 61 |
+
];
|
| 62 |
+
|
| 63 |
+
// Reactive: Filters metadata based on only_custom_metadata.
|
| 64 |
+
$: filteredMetadata = only_custom_metadata
|
| 65 |
+
? Object.fromEntries(
|
| 66 |
+
Object.entries(metadata || {}).filter(([key]) => !technicalMetadata.includes(key))
|
| 67 |
+
)
|
| 68 |
+
: metadata;
|
| 69 |
+
|
| 70 |
+
// Reactive: Calculates maximum popup width based on component width.
|
| 71 |
+
$: maxPopupWidth = typeof width === 'number' ? width - 20 : typeof width === 'string' ? `calc(${width} - 20px)` : '380px';
|
| 72 |
+
|
| 73 |
+
// Reactive: Extracts metadata when value changes.
|
| 74 |
+
$: if (value) {
|
| 75 |
+
extractMetadata(value);
|
| 76 |
+
} else {
|
| 77 |
+
metadata = null;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
/**
|
| 81 |
+
* Extracts metadata from the image file.
|
| 82 |
+
* @param fileData - Image data containing the URL.
|
| 83 |
+
*/
|
| 84 |
+
async function extractMetadata(fileData: FileData): Promise<void> {
|
| 85 |
+
if (!fileData?.url) {
|
| 86 |
+
metadata = null;
|
| 87 |
+
return;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
if (fileData.url.toLowerCase().endsWith(".svg")) {
|
| 91 |
+
metadata = null;
|
| 92 |
+
} else if (
|
| 93 |
+
fileData.url.toLowerCase().endsWith(".png") ||
|
| 94 |
+
fileData.url.toLowerCase().endsWith(".jpg") ||
|
| 95 |
+
fileData.url.toLowerCase().endsWith(".jpeg")
|
| 96 |
+
) {
|
| 97 |
+
try {
|
| 98 |
+
const exifData = await exifr.parse(fileData.url, {
|
| 99 |
+
exif: true,
|
| 100 |
+
iptc: true,
|
| 101 |
+
xmp: true,
|
| 102 |
+
});
|
| 103 |
+
metadata = {};
|
| 104 |
+
if (exifData) {
|
| 105 |
+
for (const [key, value] of Object.entries(exifData)) {
|
| 106 |
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
| 107 |
+
metadata[key] = value;
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
} catch (error) {
|
| 112 |
+
metadata = {};
|
| 113 |
+
}
|
| 114 |
+
} else {
|
| 115 |
+
metadata = {};
|
| 116 |
+
}
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
/**
|
| 120 |
+
* Handles image click to dispatch select event with coordinates.
|
| 121 |
+
* @param evt - Mouse event from clicking the image.
|
| 122 |
+
*/
|
| 123 |
+
function handle_click(evt: MouseEvent): void {
|
| 124 |
+
let coordinates = get_coordinates_of_clicked_image(evt);
|
| 125 |
+
if (coordinates) {
|
| 126 |
+
dispatch("select", { index: coordinates, value: null });
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
/**
|
| 131 |
+
* Toggles the visibility of the metadata popup.
|
| 132 |
+
*/
|
| 133 |
+
function toggleMetadataPopup(): void {
|
| 134 |
+
if (metadata !== null) {
|
| 135 |
+
showMetadataPopup = !showMetadataPopup;
|
| 136 |
+
}
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
/**
|
| 140 |
+
* Dispatches the load_metadata event and closes the popup.
|
| 141 |
+
*/
|
| 142 |
+
function dispatchLoadMetadata(): void {
|
| 143 |
+
if (metadata !== null) {
|
| 144 |
+
dispatch("load_metadata");
|
| 145 |
+
closePopup();
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
/**
|
| 150 |
+
* Closes the metadata popup.
|
| 151 |
+
*/
|
| 152 |
+
function closePopup(): void {
|
| 153 |
+
showMetadataPopup = false;
|
| 154 |
+
}
|
| 155 |
+
</script>
|
| 156 |
+
|
| 157 |
+
<BlockLabel
|
| 158 |
+
{show_label}
|
| 159 |
+
Icon={ImageIcon}
|
| 160 |
+
label={!show_label ? "" : label || i18n("image.image")}
|
| 161 |
+
/>
|
| 162 |
+
{#if value === null || !value.url}
|
| 163 |
+
<Empty unpadded_box={true} size="large"><ImageIcon /></Empty>
|
| 164 |
+
{:else}
|
| 165 |
+
<div class="image-container" bind:this={image_container} style:height style:width>
|
| 166 |
+
<IconButtonWrapper
|
| 167 |
+
display_top_corner={display_icon_button_wrapper_top_corner}
|
| 168 |
+
>
|
| 169 |
+
{#if show_fullscreen_button}
|
| 170 |
+
<FullscreenButton {fullscreen} on:fullscreen />
|
| 171 |
+
{/if}
|
| 172 |
+
{#if metadata !== null}
|
| 173 |
+
<IconButton
|
| 174 |
+
Icon={Info}
|
| 175 |
+
label="View Metadata"
|
| 176 |
+
aria-label="View and send image metadata"
|
| 177 |
+
on:click={(event) => {
|
| 178 |
+
toggleMetadataPopup();
|
| 179 |
+
event.stopPropagation();
|
| 180 |
+
}}
|
| 181 |
+
/>
|
| 182 |
+
{/if}
|
| 183 |
+
{#if show_download_button}
|
| 184 |
+
<DownloadLink href={value.url} download={value.orig_name || "image"}>
|
| 185 |
+
<IconButton Icon={Download} label={i18n("common.download")} />
|
| 186 |
+
</DownloadLink>
|
| 187 |
+
{/if}
|
| 188 |
+
{#if show_share_button}
|
| 189 |
+
<ShareButton
|
| 190 |
+
{i18n}
|
| 191 |
+
on:share
|
| 192 |
+
on:error
|
| 193 |
+
formatter={async (value) => {
|
| 194 |
+
if (!value) return "";
|
| 195 |
+
let url = await uploadToHuggingFace(value, "url");
|
| 196 |
+
return `<img src="${url}" />`;
|
| 197 |
+
}}
|
| 198 |
+
{value}
|
| 199 |
+
/>
|
| 200 |
+
{/if}
|
| 201 |
+
</IconButtonWrapper>
|
| 202 |
+
<button on:click={handle_click}>
|
| 203 |
+
<div class:selectable class="image-frame">
|
| 204 |
+
<Image src={value.url} alt="" loading="lazy" on:load />
|
| 205 |
+
</div>
|
| 206 |
+
</button>
|
| 207 |
+
</div>
|
| 208 |
+
{/if}
|
| 209 |
+
|
| 210 |
+
{#if showMetadataPopup && filteredMetadata !== null}
|
| 211 |
+
<div
|
| 212 |
+
class="metadata-popup"
|
| 213 |
+
style:width={typeof popup_metadata_width === 'number' ? `${Math.min(popup_metadata_width, parseFloat(maxPopupWidth))}px` : `min(${popup_metadata_width}, ${maxPopupWidth})`}
|
| 214 |
+
style:height={typeof popup_metadata_height === 'number' ? `${popup_metadata_height}px` : popup_metadata_height}
|
| 215 |
+
>
|
| 216 |
+
<div class="popup-content">
|
| 217 |
+
<button class="close-button" on:click={closePopup}>X</button>
|
| 218 |
+
<h3 class="popup-title">Image Metadata</h3>
|
| 219 |
+
{#if filteredMetadata.error}
|
| 220 |
+
<p>{filteredMetadata.error}</p>
|
| 221 |
+
{:else}
|
| 222 |
+
<div class="metadata-table-container">
|
| 223 |
+
<table class="metadata-table">
|
| 224 |
+
<tbody>
|
| 225 |
+
{#each Object.entries(filteredMetadata) as [key, val]}
|
| 226 |
+
{#if val}
|
| 227 |
+
<tr>
|
| 228 |
+
<td class="metadata-label">{key}</td>
|
| 229 |
+
<td class="metadata-value">{val}</td>
|
| 230 |
+
</tr>
|
| 231 |
+
{/if}
|
| 232 |
+
{/each}
|
| 233 |
+
</tbody>
|
| 234 |
+
</table>
|
| 235 |
+
</div>
|
| 236 |
+
<button class="load-metadata-button" on:click={dispatchLoadMetadata}>Load Metadata</button>
|
| 237 |
+
{/if}
|
| 238 |
+
</div>
|
| 239 |
+
</div>
|
| 240 |
+
{/if}
|
| 241 |
+
|
| 242 |
+
<style>
|
| 243 |
+
.image-container {
|
| 244 |
+
height: 100%;
|
| 245 |
+
position: relative;
|
| 246 |
+
min-width: var(--size-20);
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
.image-container button {
|
| 250 |
+
width: var(--size-full);
|
| 251 |
+
height: var(--size-full);
|
| 252 |
+
border-radius: var(--radius-lg);
|
| 253 |
+
display: flex;
|
| 254 |
+
align-items: center;
|
| 255 |
+
justify-content: center;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.image-frame :global(img) {
|
| 259 |
+
width: var(--size-full);
|
| 260 |
+
height: var(--size-full);
|
| 261 |
+
object-fit: scale-down;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
.selectable {
|
| 265 |
+
cursor: crosshair;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
:global(.fullscreen-controls svg) {
|
| 269 |
+
position: relative;
|
| 270 |
+
top: 0px;
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
:global(.image-container:fullscreen) {
|
| 274 |
+
background-color: black;
|
| 275 |
+
display: flex;
|
| 276 |
+
justify-content: center;
|
| 277 |
+
align-items: center;
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
:global(.image-container:fullscreen img) {
|
| 281 |
+
max-width: 90vw;
|
| 282 |
+
max-height: 90vh;
|
| 283 |
+
object-fit: scale-down;
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
.image-frame {
|
| 287 |
+
width: auto;
|
| 288 |
+
height: 100%;
|
| 289 |
+
display: flex;
|
| 290 |
+
align-items: center;
|
| 291 |
+
justify-content: center;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
.metadata-popup {
|
| 295 |
+
position: absolute;
|
| 296 |
+
top: 50%;
|
| 297 |
+
left: 50%;
|
| 298 |
+
transform: translate(-50%, -50%);
|
| 299 |
+
background: var(--background-fill-primary, white);
|
| 300 |
+
border: 1px solid var(--border-color-primary);
|
| 301 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
| 302 |
+
z-index: 1000;
|
| 303 |
+
border-radius: 8px;
|
| 304 |
+
overflow: hidden;
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
.popup-content {
|
| 308 |
+
position: relative;
|
| 309 |
+
padding: 1rem;
|
| 310 |
+
display: flex;
|
| 311 |
+
flex-direction: column;
|
| 312 |
+
height: 100%;
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
.popup-title {
|
| 316 |
+
font-weight: bold;
|
| 317 |
+
margin: 0 0 1rem 0;
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
.close-button {
|
| 321 |
+
position: absolute;
|
| 322 |
+
top: 0.5rem;
|
| 323 |
+
right: 0.5rem;
|
| 324 |
+
background: none;
|
| 325 |
+
border: none;
|
| 326 |
+
font-size: 1rem;
|
| 327 |
+
cursor: pointer;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
.metadata-table-container {
|
| 331 |
+
flex: 1;
|
| 332 |
+
overflow: auto;
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
.metadata-table {
|
| 336 |
+
width: 100%;
|
| 337 |
+
border-collapse: collapse;
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
.metadata-label {
|
| 341 |
+
background: var(--background-fill-secondary, #f5f5f5);
|
| 342 |
+
padding: 0.5rem;
|
| 343 |
+
font-weight: bold;
|
| 344 |
+
text-align: left;
|
| 345 |
+
vertical-align: top;
|
| 346 |
+
width: 40%;
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
.metadata-value {
|
| 350 |
+
padding: 0.5rem;
|
| 351 |
+
white-space: nowrap;
|
| 352 |
+
vertical-align: top;
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
.load-metadata-button {
|
| 356 |
+
margin-top: 1rem;
|
| 357 |
+
padding: 0.5rem 1rem;
|
| 358 |
+
background-color: var(--button-primary-background-fill);
|
| 359 |
+
color: var(--button-primary-text-color);
|
| 360 |
+
border: none;
|
| 361 |
+
border-radius: 4px;
|
| 362 |
+
cursor: pointer;
|
| 363 |
+
align-self: center;
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
.load-metadata-button:hover {
|
| 367 |
+
background-color: var(--button-primary-background-fill-hover);
|
| 368 |
+
}
|
| 369 |
+
</style>
|
src/frontend/shared/ImageUploader.svelte
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import { createEventDispatcher, tick } from "svelte";
|
| 3 |
+
import { BlockLabel, IconButtonWrapper, IconButton } from "@gradio/atoms";
|
| 4 |
+
import { Clear, Image as ImageIcon, Info } from "@gradio/icons";
|
| 5 |
+
import { FullscreenButton } from "@gradio/atoms";
|
| 6 |
+
import { Upload } from "@gradio/upload";
|
| 7 |
+
import type { Client } from "@gradio/client";
|
| 8 |
+
import Image from "./Image.svelte";
|
| 9 |
+
import * as exifr from "exifr";
|
| 10 |
+
import { get_coordinates_of_clicked_image } from "./utils";
|
| 11 |
+
|
| 12 |
+
/**
|
| 13 |
+
* Props for the ImageUploader component.
|
| 14 |
+
*/
|
| 15 |
+
export let value: null | any = null; // Image data with URL, path, and optional metadata.
|
| 16 |
+
export let label: string | undefined = undefined; // Label displayed above the component.
|
| 17 |
+
export let show_label: boolean; // Whether to display the label.
|
| 18 |
+
export let selectable: boolean = false; // Whether the image is clickable for coordinate selection.
|
| 19 |
+
export let root: string; // Root URL for file uploads.
|
| 20 |
+
export let i18n: any; // Formatter for internationalization.
|
| 21 |
+
export let max_file_size: number | null = null; // Maximum file size for uploads (in bytes).
|
| 22 |
+
export let upload: Client["upload"]; // Function to handle file uploads.
|
| 23 |
+
export let stream_handler: Client["stream"]; // Function to handle webcam streaming.
|
| 24 |
+
export let show_fullscreen_button: boolean = true; // Whether to show the fullscreen button.
|
| 25 |
+
export let height: number | string | undefined = undefined; // Component height (pixels or CSS units).
|
| 26 |
+
export let width: number | string | undefined = undefined; // Component width (pixels or CSS units).
|
| 27 |
+
export let only_custom_metadata: boolean = true; // Whether to filter out technical metadata.
|
| 28 |
+
export let popup_metadata_width: number | string = 400; // Width of the metadata popup (pixels or CSS units).
|
| 29 |
+
export let popup_metadata_height: number | string = 300; // Height of the metadata popup (pixels or CSS units).
|
| 30 |
+
|
| 31 |
+
let upload_input: Upload; // Reference to the Upload component.
|
| 32 |
+
export let uploading: boolean = false; // Whether an upload is in progress.
|
| 33 |
+
export let active_source: "upload" = "upload"; // Current input source (only "upload" supported).
|
| 34 |
+
export let fullscreen: boolean = false; // Whether the image is in fullscreen mode.
|
| 35 |
+
let metadata: Record<string, any> | null = null; // Extracted image metadata.
|
| 36 |
+
let showMetadataPopup: boolean = false; // Whether the metadata popup is visible.
|
| 37 |
+
let image_container: HTMLElement; // Reference to the image container element.
|
| 38 |
+
export let dragging: boolean = false; // Whether a file is being dragged over the component.
|
| 39 |
+
|
| 40 |
+
// Event dispatcher for component events.
|
| 41 |
+
const dispatch = createEventDispatcher<{
|
| 42 |
+
change?: never; // Triggered when the image changes.
|
| 43 |
+
clear?: never; // Triggered when the image is cleared.
|
| 44 |
+
drag: boolean; // Triggered when dragging state changes.
|
| 45 |
+
upload?: never; // Triggered after a successful upload.
|
| 46 |
+
select: any; // Triggered when clicking the image with coordinates.
|
| 47 |
+
load_metadata: never; // Triggered when loading metadata.
|
| 48 |
+
}>();
|
| 49 |
+
|
| 50 |
+
// Reactive: Dispatches drag event when dragging state changes.
|
| 51 |
+
$: dispatch("drag", dragging);
|
| 52 |
+
|
| 53 |
+
// List of technical metadata keys to filter out when only_custom_metadata is true.
|
| 54 |
+
const technicalMetadata: string[] = [
|
| 55 |
+
"ImageWidth",
|
| 56 |
+
"ImageHeight",
|
| 57 |
+
"BitDepth",
|
| 58 |
+
"ColorType",
|
| 59 |
+
"Compression",
|
| 60 |
+
"Filter",
|
| 61 |
+
"Interlace"
|
| 62 |
+
];
|
| 63 |
+
|
| 64 |
+
// Reactive: Filters metadata based on only_custom_metadata.
|
| 65 |
+
$: filteredMetadata = only_custom_metadata
|
| 66 |
+
? Object.fromEntries(
|
| 67 |
+
Object.entries(metadata || {}).filter(([key]) => !technicalMetadata.includes(key))
|
| 68 |
+
)
|
| 69 |
+
: metadata;
|
| 70 |
+
|
| 71 |
+
// Reactive: Calculates maximum popup width based on component width.
|
| 72 |
+
$: maxPopupWidth = typeof width === 'number' ? width - 20 : typeof width === 'string' ? `calc(${width} - 20px)` : '380px';
|
| 73 |
+
|
| 74 |
+
// Reactive: Extracts metadata when value changes (e.g., from gr.Examples).
|
| 75 |
+
$: if (value) {
|
| 76 |
+
// Use preloaded metadata if available, otherwise extract from URL
|
| 77 |
+
if (value.metadata) {
|
| 78 |
+
metadata = value.metadata;
|
| 79 |
+
} else {
|
| 80 |
+
extractMetadata(value);
|
| 81 |
+
}
|
| 82 |
+
} else {
|
| 83 |
+
metadata = null;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
/**
|
| 87 |
+
* Extracts metadata from the image file.
|
| 88 |
+
* @param fileData - Image data containing the URL or path.
|
| 89 |
+
*/
|
| 90 |
+
async function extractMetadata(fileData: any): Promise<void> {
|
| 91 |
+
if (!fileData?.url) {
|
| 92 |
+
metadata = null;
|
| 93 |
+
return;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
if (fileData.url.toLowerCase().endsWith(".svg")) {
|
| 97 |
+
metadata = null;
|
| 98 |
+
} else if (
|
| 99 |
+
fileData.url.toLowerCase().endsWith(".png") ||
|
| 100 |
+
fileData.url.toLowerCase().endsWith(".jpg") ||
|
| 101 |
+
fileData.url.toLowerCase().endsWith(".jpeg")
|
| 102 |
+
) {
|
| 103 |
+
try {
|
| 104 |
+
const exifData = await exifr.parse(fileData.url, {
|
| 105 |
+
exif: true,
|
| 106 |
+
iptc: true,
|
| 107 |
+
xmp: true,
|
| 108 |
+
});
|
| 109 |
+
metadata = {};
|
| 110 |
+
if (exifData) {
|
| 111 |
+
for (const [key, value] of Object.entries(exifData)) {
|
| 112 |
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
| 113 |
+
metadata[key] = value;
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
} catch (error) {
|
| 118 |
+
metadata = { error: "Failed to extract metadata" };
|
| 119 |
+
}
|
| 120 |
+
} else {
|
| 121 |
+
metadata = { error: "Unsupported file type" };
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
/**
|
| 126 |
+
* Handles file upload and extracts metadata for supported image formats.
|
| 127 |
+
* @param event - Custom event containing uploaded file data.
|
| 128 |
+
*/
|
| 129 |
+
async function handle_upload({ detail }: CustomEvent<any>): Promise<void> {
|
| 130 |
+
let extractedMetadata = null;
|
| 131 |
+
if (detail.path?.toLowerCase().endsWith(".svg") && detail.url) {
|
| 132 |
+
const response = await fetch(detail.url);
|
| 133 |
+
const svgContent = await response.text();
|
| 134 |
+
value = {
|
| 135 |
+
...detail,
|
| 136 |
+
url: `data:image/svg+xml,${encodeURIComponent(svgContent)}`,
|
| 137 |
+
metadata: null
|
| 138 |
+
};
|
| 139 |
+
extractedMetadata = null;
|
| 140 |
+
} else if (
|
| 141 |
+
detail.url &&
|
| 142 |
+
(detail.path?.toLowerCase().endsWith(".png") ||
|
| 143 |
+
detail.path?.toLowerCase().endsWith(".jpg") ||
|
| 144 |
+
detail.path?.toLowerCase().endsWith(".jpeg"))
|
| 145 |
+
) {
|
| 146 |
+
try {
|
| 147 |
+
const exifData = await exifr.parse(detail.url, {
|
| 148 |
+
exif: true,
|
| 149 |
+
iptc: true,
|
| 150 |
+
xmp: true,
|
| 151 |
+
});
|
| 152 |
+
extractedMetadata = {};
|
| 153 |
+
if (exifData) {
|
| 154 |
+
for (const [key, value] of Object.entries(exifData)) {
|
| 155 |
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
| 156 |
+
extractedMetadata[key] = value;
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
} catch (error) {
|
| 161 |
+
extractedMetadata = { error: "Failed to extract metadata" };
|
| 162 |
+
}
|
| 163 |
+
} else {
|
| 164 |
+
extractedMetadata = { error: "Unsupported file type" };
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
value = {
|
| 168 |
+
...detail,
|
| 169 |
+
metadata: extractedMetadata
|
| 170 |
+
};
|
| 171 |
+
metadata = extractedMetadata;
|
| 172 |
+
await tick();
|
| 173 |
+
dispatch("upload");
|
| 174 |
+
dispatch("change", value);
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
/**
|
| 178 |
+
* Clears the current image and metadata.
|
| 179 |
+
*/
|
| 180 |
+
function handle_clear(): void {
|
| 181 |
+
value = null;
|
| 182 |
+
metadata = null;
|
| 183 |
+
showMetadataPopup = false;
|
| 184 |
+
dispatch("clear");
|
| 185 |
+
dispatch("change", null);
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
/**
|
| 189 |
+
* Handles image click to dispatch select event with coordinates.
|
| 190 |
+
* @param evt - Mouse event from clicking the image.
|
| 191 |
+
*/
|
| 192 |
+
function handle_click(evt: MouseEvent): void {
|
| 193 |
+
let coordinates = get_coordinates_of_clicked_image(evt);
|
| 194 |
+
if (coordinates) {
|
| 195 |
+
dispatch("select", { index: coordinates, value: null });
|
| 196 |
+
}
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
/**
|
| 200 |
+
* Handles drag-over event to enable file drop.
|
| 201 |
+
* @param evt - Drag event.
|
| 202 |
+
*/
|
| 203 |
+
function on_drag_over(evt: DragEvent): void {
|
| 204 |
+
evt.preventDefault();
|
| 205 |
+
evt.stopPropagation();
|
| 206 |
+
if (evt.dataTransfer) {
|
| 207 |
+
evt.dataTransfer.dropEffect = "copy";
|
| 208 |
+
}
|
| 209 |
+
dragging = true;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
/**
|
| 213 |
+
* Handles file drop to initiate upload.
|
| 214 |
+
* @param evt - Drag event containing dropped files.
|
| 215 |
+
*/
|
| 216 |
+
async function on_drop(evt: DragEvent): Promise<void> {
|
| 217 |
+
evt.preventDefault();
|
| 218 |
+
evt.stopPropagation();
|
| 219 |
+
dragging = false;
|
| 220 |
+
|
| 221 |
+
if (value) {
|
| 222 |
+
handle_clear();
|
| 223 |
+
await tick();
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
active_source = "upload";
|
| 227 |
+
await tick();
|
| 228 |
+
upload_input.load_files_from_drop(evt);
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
/**
|
| 232 |
+
* Toggles the visibility of the metadata popup.
|
| 233 |
+
*/
|
| 234 |
+
function toggleMetadataPopup(): void {
|
| 235 |
+
if (metadata !== null) {
|
| 236 |
+
showMetadataPopup = !showMetadataPopup;
|
| 237 |
+
}
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
/**
|
| 241 |
+
* Dispatches the load_metadata event and closes the popup.
|
| 242 |
+
*/
|
| 243 |
+
function dispatchLoadMetadata(): void {
|
| 244 |
+
if (metadata !== null) {
|
| 245 |
+
dispatch("load_metadata");
|
| 246 |
+
closePopup();
|
| 247 |
+
}
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
/**
|
| 251 |
+
* Closes the metadata popup.
|
| 252 |
+
*/
|
| 253 |
+
function closePopup(): void {
|
| 254 |
+
showMetadataPopup = false;
|
| 255 |
+
}
|
| 256 |
+
</script>
|
| 257 |
+
|
| 258 |
+
<BlockLabel {show_label} Icon={ImageIcon} label={label || "Image"} />
|
| 259 |
+
|
| 260 |
+
<div data-testid="image" class="image-container" bind:this={image_container} style:height style:width>
|
| 261 |
+
<IconButtonWrapper>
|
| 262 |
+
{#if value?.url}
|
| 263 |
+
{#if show_fullscreen_button}
|
| 264 |
+
<FullscreenButton {fullscreen} on:fullscreen />
|
| 265 |
+
{/if}
|
| 266 |
+
{#if metadata !== null}
|
| 267 |
+
<IconButton
|
| 268 |
+
Icon={Info}
|
| 269 |
+
label="View Metadata"
|
| 270 |
+
aria-label="View and send image metadata"
|
| 271 |
+
on:click={(event) => {
|
| 272 |
+
toggleMetadataPopup();
|
| 273 |
+
event.stopPropagation();
|
| 274 |
+
}}
|
| 275 |
+
/>
|
| 276 |
+
{/if}
|
| 277 |
+
<IconButton
|
| 278 |
+
Icon={Clear}
|
| 279 |
+
label="Remove Image"
|
| 280 |
+
on:click={(event) => {
|
| 281 |
+
handle_clear();
|
| 282 |
+
event.stopPropagation();
|
| 283 |
+
}}
|
| 284 |
+
/>
|
| 285 |
+
{/if}
|
| 286 |
+
</IconButtonWrapper>
|
| 287 |
+
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
| 288 |
+
<div
|
| 289 |
+
class="upload-container"
|
| 290 |
+
style:width={value ? "auto" : "100%"}
|
| 291 |
+
on:dragover={on_drag_over}
|
| 292 |
+
on:drop={on_drop}
|
| 293 |
+
>
|
| 294 |
+
<Upload
|
| 295 |
+
hidden={value !== null}
|
| 296 |
+
bind:this={upload_input}
|
| 297 |
+
bind:uploading
|
| 298 |
+
bind:dragging
|
| 299 |
+
filetype="image/*"
|
| 300 |
+
on:load={handle_upload}
|
| 301 |
+
on:error
|
| 302 |
+
{root}
|
| 303 |
+
{max_file_size}
|
| 304 |
+
disable_click={value !== null}
|
| 305 |
+
{upload}
|
| 306 |
+
{stream_handler}
|
| 307 |
+
aria_label={i18n("image.drop_to_upload")}
|
| 308 |
+
>
|
| 309 |
+
{#if value === null}
|
| 310 |
+
<slot />
|
| 311 |
+
{/if}
|
| 312 |
+
</Upload>
|
| 313 |
+
{#if value !== null}
|
| 314 |
+
<!-- svelte-ignore a11y-click-events-have-key-events-->
|
| 315 |
+
<!-- svelte-ignore a11y-no-static-element-interactions-->
|
| 316 |
+
<div class:selectable class="image-frame" on:click={handle_click}>
|
| 317 |
+
<Image src={value.url} alt={value.alt_text} />
|
| 318 |
+
</div>
|
| 319 |
+
{/if}
|
| 320 |
+
</div>
|
| 321 |
+
</div>
|
| 322 |
+
|
| 323 |
+
{#if showMetadataPopup && filteredMetadata !== null}
|
| 324 |
+
<div
|
| 325 |
+
class="metadata-popup"
|
| 326 |
+
style:width={typeof popup_metadata_width === 'number' ? `${Math.min(popup_metadata_width, parseFloat(maxPopupWidth))}px` : `min(${popup_metadata_width}, ${maxPopupWidth})`}
|
| 327 |
+
style:height={typeof popup_metadata_height === 'number' ? `${popup_metadata_height}px` : popup_metadata_height}
|
| 328 |
+
>
|
| 329 |
+
<div class="popup-content">
|
| 330 |
+
<button class="close-button" on:click={closePopup}>X</button>
|
| 331 |
+
<h3 class="popup-title">Image Metadata</h3>
|
| 332 |
+
{#if filteredMetadata.error}
|
| 333 |
+
<p>{filteredMetadata.error}</p>
|
| 334 |
+
{:else}
|
| 335 |
+
<div class="metadata-table-container">
|
| 336 |
+
<table class="metadata-table">
|
| 337 |
+
<tbody>
|
| 338 |
+
{#each Object.entries(filteredMetadata) as [key, val]}
|
| 339 |
+
{#if val}
|
| 340 |
+
<tr>
|
| 341 |
+
<td class="metadata-label">{key}</td>
|
| 342 |
+
<td class="metadata-value">{val}</td>
|
| 343 |
+
</tr>
|
| 344 |
+
{/if}
|
| 345 |
+
{/each}
|
| 346 |
+
</tbody>
|
| 347 |
+
</table>
|
| 348 |
+
</div>
|
| 349 |
+
<button class="load-metadata-button" on:click={dispatchLoadMetadata}>Load Metadata</button>
|
| 350 |
+
{/if}
|
| 351 |
+
</div>
|
| 352 |
+
</div>
|
| 353 |
+
{/if}
|
| 354 |
+
|
| 355 |
+
<style>
|
| 356 |
+
.image-frame :global(img) {
|
| 357 |
+
width: var(--size-full);
|
| 358 |
+
height: var(--size-full);
|
| 359 |
+
object-fit: scale-down;
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
.upload-container {
|
| 363 |
+
display: flex;
|
| 364 |
+
align-items: center;
|
| 365 |
+
justify-content: center;
|
| 366 |
+
height: 100%;
|
| 367 |
+
flex-shrink: 1;
|
| 368 |
+
max-height: 100%;
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
.image-container {
|
| 372 |
+
display: flex;
|
| 373 |
+
height: 100%;
|
| 374 |
+
flex-direction: column;
|
| 375 |
+
justify-content: center;
|
| 376 |
+
align-items: center;
|
| 377 |
+
max-height: 100%;
|
| 378 |
+
position: relative;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
.selectable {
|
| 382 |
+
cursor: crosshair;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
.image-frame {
|
| 386 |
+
object-fit: cover;
|
| 387 |
+
width: 100%;
|
| 388 |
+
height: 100%;
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
.metadata-popup {
|
| 392 |
+
position: absolute;
|
| 393 |
+
top: 50%;
|
| 394 |
+
left: 50%;
|
| 395 |
+
transform: translate(-50%, -50%);
|
| 396 |
+
background: var(--background-fill-primary, white);
|
| 397 |
+
border: 1px solid var(--border-color-primary);
|
| 398 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
| 399 |
+
z-index: 1000;
|
| 400 |
+
border-radius: 8px;
|
| 401 |
+
overflow: hidden;
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
.popup-content {
|
| 405 |
+
position: relative;
|
| 406 |
+
padding: 1rem;
|
| 407 |
+
display: flex;
|
| 408 |
+
flex-direction: column;
|
| 409 |
+
height: 100%;
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.popup-title {
|
| 413 |
+
font-weight: bold;
|
| 414 |
+
margin: 0 0 1rem 0;
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
.close-button {
|
| 418 |
+
position: absolute;
|
| 419 |
+
top: 0.5rem;
|
| 420 |
+
right: 0.5rem;
|
| 421 |
+
background: none;
|
| 422 |
+
border: none;
|
| 423 |
+
font-size: 1rem;
|
| 424 |
+
cursor: pointer;
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
.metadata-table-container {
|
| 428 |
+
flex: 1;
|
| 429 |
+
overflow: auto;
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
.metadata-table {
|
| 433 |
+
width: 100%;
|
| 434 |
+
border-collapse: collapse;
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
.metadata-label {
|
| 438 |
+
background: var(--background-fill-secondary, #f5f5f5);
|
| 439 |
+
padding: 0.5rem;
|
| 440 |
+
font-weight: bold;
|
| 441 |
+
text-align: left;
|
| 442 |
+
vertical-align: top;
|
| 443 |
+
width: 40%;
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
.metadata-value {
|
| 447 |
+
padding: 0.5rem;
|
| 448 |
+
white-space: nowrap;
|
| 449 |
+
vertical-align: top;
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
.load-metadata-button {
|
| 453 |
+
margin-top: 1rem;
|
| 454 |
+
padding: 0.5rem 1rem;
|
| 455 |
+
background-color: var(--button-primary-background-fill);
|
| 456 |
+
color: var(--button-primary-text-color);
|
| 457 |
+
border: none;
|
| 458 |
+
border-radius: 4px;
|
| 459 |
+
cursor: pointer;
|
| 460 |
+
align-self: center;
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
.load-metadata-button:hover {
|
| 464 |
+
background-color: var(--button-primary-background-fill-hover);
|
| 465 |
+
}
|
| 466 |
+
</style>
|
src/frontend/shared/index.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export { default as Image } from "./Image.svelte";
|
| 2 |
+
export { default as StaticImage } from "./ImagePreview.svelte";
|
src/frontend/shared/types.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export interface Base64File {
|
| 2 |
+
url: string;
|
| 3 |
+
alt_text: string;
|
| 4 |
+
}
|
| 5 |
+
|
src/frontend/shared/utils.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const get_coordinates_of_clicked_image = (
|
| 2 |
+
evt: MouseEvent
|
| 3 |
+
): [number, number] | null => {
|
| 4 |
+
let image;
|
| 5 |
+
if (evt.currentTarget instanceof Element) {
|
| 6 |
+
image = evt.currentTarget.querySelector("img") as HTMLImageElement;
|
| 7 |
+
} else {
|
| 8 |
+
return [NaN, NaN];
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
const imageRect = image.getBoundingClientRect();
|
| 12 |
+
const xScale = image.naturalWidth / imageRect.width;
|
| 13 |
+
const yScale = image.naturalHeight / imageRect.height;
|
| 14 |
+
if (xScale > yScale) {
|
| 15 |
+
const displayed_height = image.naturalHeight / xScale;
|
| 16 |
+
const y_offset = (imageRect.height - displayed_height) / 2;
|
| 17 |
+
var x = Math.round((evt.clientX - imageRect.left) * xScale);
|
| 18 |
+
var y = Math.round((evt.clientY - imageRect.top - y_offset) * xScale);
|
| 19 |
+
} else {
|
| 20 |
+
const displayed_width = image.naturalWidth / yScale;
|
| 21 |
+
const x_offset = (imageRect.width - displayed_width) / 2;
|
| 22 |
+
var x = Math.round((evt.clientX - imageRect.left - x_offset) * yScale);
|
| 23 |
+
var y = Math.round((evt.clientY - imageRect.top) * yScale);
|
| 24 |
+
}
|
| 25 |
+
if (x < 0 || x >= image.naturalWidth || y < 0 || y >= image.naturalHeight) {
|
| 26 |
+
return null;
|
| 27 |
+
}
|
| 28 |
+
return [x, y];
|
| 29 |
+
};
|
src/frontend/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"allowJs": true,
|
| 4 |
+
"checkJs": true,
|
| 5 |
+
"esModuleInterop": true,
|
| 6 |
+
"forceConsistentCasingInFileNames": true,
|
| 7 |
+
"resolveJsonModule": true,
|
| 8 |
+
"skipLibCheck": true,
|
| 9 |
+
"sourceMap": true,
|
| 10 |
+
"strict": true,
|
| 11 |
+
"verbatimModuleSyntax": true
|
| 12 |
+
},
|
| 13 |
+
"exclude": ["node_modules", "dist", "./gradio.config.js"]
|
| 14 |
+
}
|
src/pyproject.toml
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = [
|
| 3 |
+
"hatchling",
|
| 4 |
+
"hatch-requirements-txt",
|
| 5 |
+
"hatch-fancy-pypi-readme>=22.5.0",
|
| 6 |
+
]
|
| 7 |
+
build-backend = "hatchling.build"
|
| 8 |
+
|
| 9 |
+
[project]
|
| 10 |
+
name = "gradio_imagemeta"
|
| 11 |
+
version = "0.0.1"
|
| 12 |
+
description = "Image Preview with Metadata for Gradio Interface"
|
| 13 |
+
readme = "README.md"
|
| 14 |
+
license = "apache-2.0"
|
| 15 |
+
requires-python = ">=3.10"
|
| 16 |
+
authors = [{ name = "Eliseu Silva", email = "elismasilva@gmail.com" }]
|
| 17 |
+
keywords = ["gradio-custom-component", "gradio-template-Image"]
|
| 18 |
+
# Add dependencies here
|
| 19 |
+
dependencies = ["gradio>=4.0,<6.0"]
|
| 20 |
+
classifiers = [
|
| 21 |
+
'Development Status :: 3 - Alpha',
|
| 22 |
+
'Operating System :: OS Independent',
|
| 23 |
+
'Programming Language :: Python :: 3',
|
| 24 |
+
'Programming Language :: Python :: 3 :: Only',
|
| 25 |
+
'Programming Language :: Python :: 3.8',
|
| 26 |
+
'Programming Language :: Python :: 3.9',
|
| 27 |
+
'Programming Language :: Python :: 3.10',
|
| 28 |
+
'Programming Language :: Python :: 3.11',
|
| 29 |
+
'Topic :: Scientific/Engineering',
|
| 30 |
+
'Topic :: Scientific/Engineering :: Artificial Intelligence',
|
| 31 |
+
'Topic :: Scientific/Engineering :: Visualization',
|
| 32 |
+
]
|
| 33 |
+
|
| 34 |
+
# The repository and space URLs are optional, but recommended.
|
| 35 |
+
# Adding a repository URL will create a badge in the auto-generated README that links to the repository.
|
| 36 |
+
# Adding a space URL will create a badge in the auto-generated README that links to the space.
|
| 37 |
+
# This will make it easy for people to find your deployed demo or source code when they
|
| 38 |
+
# encounter your project in the wild.
|
| 39 |
+
|
| 40 |
+
# [project.urls]
|
| 41 |
+
# repository = "https://github.com/DEVAIEXP/gradio_component_imagemeta"
|
| 42 |
+
# space = "https://huggingface.co/spaces/elismasilva/gradio_imagemeta"
|
| 43 |
+
|
| 44 |
+
[project.optional-dependencies]
|
| 45 |
+
dev = ["build", "twine"]
|
| 46 |
+
|
| 47 |
+
[tool.hatch.build]
|
| 48 |
+
artifacts = ["/backend/gradio_imagemeta/templates", "*.pyi", "/\\backend\\gradio_imagemeta\\templates"]
|
| 49 |
+
|
| 50 |
+
[tool.hatch.build.targets.wheel]
|
| 51 |
+
packages = ["/backend/gradio_imagemeta"]
|