elismasilva commited on
Commit
1751b5b
·
verified ·
1 Parent(s): 6d06c80

Upload folder using huggingface_hub

Browse files
Files changed (41) hide show
  1. .gitattributes +3 -0
  2. .gitignore +15 -0
  3. README.md +673 -12
  4. __init__.py +0 -0
  5. app.py +216 -0
  6. css.css +157 -0
  7. requirements.txt +2 -0
  8. space.py +344 -0
  9. src/.gitignore +15 -0
  10. src/.gradio/cached_examples/24/Upload Image All metadata/7a159874ef63b94d8fa4/image_with_meta.png +3 -0
  11. src/.gradio/cached_examples/24/Upload Image Custom metadata only/8a0de8f0e921a79b67a8/image_with_meta.png +3 -0
  12. src/.gradio/cached_examples/24/indices.csv +1 -0
  13. src/.gradio/cached_examples/24/log.csv +2 -0
  14. src/.vscode/launch.json +29 -0
  15. src/README.md +673 -0
  16. src/backend/gradio_imagemeta/__init__.py +4 -0
  17. src/backend/gradio_imagemeta/helpers.py +182 -0
  18. src/backend/gradio_imagemeta/imagemeta.py +231 -0
  19. src/backend/gradio_imagemeta/templates/component/index.js +0 -0
  20. src/backend/gradio_imagemeta/templates/component/style.css +1 -0
  21. src/backend/gradio_imagemeta/templates/example/index.js +316 -0
  22. src/backend/gradio_imagemeta/templates/example/style.css +1 -0
  23. src/demo/__init__.py +0 -0
  24. src/demo/app.py +216 -0
  25. src/demo/css.css +157 -0
  26. src/demo/requirements.txt +2 -0
  27. src/demo/space.py +344 -0
  28. src/examples/image_with_meta.png +3 -0
  29. src/frontend/Example.svelte +49 -0
  30. src/frontend/Index.svelte +246 -0
  31. src/frontend/gradio.config.js +9 -0
  32. src/frontend/package-lock.json +0 -0
  33. src/frontend/package.json +58 -0
  34. src/frontend/shared/Image.svelte +44 -0
  35. src/frontend/shared/ImagePreview.svelte +369 -0
  36. src/frontend/shared/ImageUploader.svelte +466 -0
  37. src/frontend/shared/index.ts +2 -0
  38. src/frontend/shared/types.ts +5 -0
  39. src/frontend/shared/utils.ts +29 -0
  40. src/frontend/tsconfig.json +14 -0
  41. 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
- title: Gradio Imagemeta
3
- emoji: 🌍
4
- colorFrom: indigo
5
- colorTo: blue
6
- sdk: gradio
7
- sdk_version: 5.42.0
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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

  • SHA256: 21d3f57b5657cbdbfcc65be66db9793ec5592cf7d375468c1dd4677147dad769
  • Pointer size: 132 Bytes
  • Size of remote file: 5.79 MB
src/.gradio/cached_examples/24/Upload Image Custom metadata only/8a0de8f0e921a79b67a8/image_with_meta.png ADDED

Git LFS Details

  • SHA256: 21d3f57b5657cbdbfcc65be66db9793ec5592cf7d375468c1dd4677147dad769
  • Pointer size: 132 Bytes
  • Size of remote file: 5.79 MB
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

  • SHA256: 21d3f57b5657cbdbfcc65be66db9793ec5592cf7d375468c1dd4677147dad769
  • Pointer size: 132 Bytes
  • Size of remote file: 5.79 MB
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"]