Skip to content

Commit 9045237

Browse files
feat(api): add util to extract metadata from image
1 parent 58959a1 commit 9045237

File tree

1 file changed

+108
-0
lines changed

1 file changed

+108
-0
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import json
2+
import logging
3+
from dataclasses import dataclass
4+
5+
from PIL import Image
6+
7+
from invokeai.app.services.shared.graph import Graph
8+
from invokeai.app.services.workflow_records.workflow_records_common import WorkflowWithoutIDValidator
9+
10+
11+
@dataclass
12+
class ExtractedMetadata:
13+
invokeai_metadata: str | None
14+
invokeai_workflow: str | None
15+
invokeai_graph: str | None
16+
17+
18+
def extract_metadata_from_image(
19+
pil_image: Image.Image,
20+
invokeai_metadata_override: str | None,
21+
invokeai_workflow_override: str | None,
22+
invokeai_graph_override: str | None,
23+
logger: logging.Logger,
24+
) -> ExtractedMetadata:
25+
"""
26+
Extracts the "invokeai_metadata", "invokeai_workflow", and "invokeai_graph" data embedded in the PIL Image.
27+
28+
These items are stored as stringified JSON in the image file's metadata, so we need to do some parsing to validate
29+
them. Once parsed, the values are returned as they came (as strings), or None if they are not present or invalid.
30+
31+
In some situations, we may prefer to override the values extracted from the image file with some other values.
32+
33+
For example, when uploading an image via API, the client can optionally provide the metadata directly in the request,
34+
as opposed to embedding it in the image file. In this case, the client-provided metadata will be used instead of the
35+
metadata embedded in the image file.
36+
37+
Args:
38+
pil_image: The PIL Image object.
39+
invokeai_metadata_override: The metadata override provided by the client.
40+
invokeai_workflow_override: The workflow override provided by the client.
41+
invokeai_graph_override: The graph override provided by the client.
42+
logger: The logger to use for debug logging.
43+
44+
Returns:
45+
ExtractedMetadata: The extracted metadata, workflow, and graph.
46+
"""
47+
48+
# The fallback value for metadata is None.
49+
stringified_metadata: str | None = None
50+
51+
# Use the metadata override if provided, else attempt to extract it from the image file.
52+
metadata_raw = invokeai_metadata_override or pil_image.info.get("invokeai_metadata", None)
53+
54+
# If the metadata is present in the image file, we will attempt to parse it as JSON. When we create images,
55+
# we always store metadata as a stringified JSON dict. So, we expect it to be a string here.
56+
if isinstance(metadata_raw, str):
57+
try:
58+
# Must be a JSON string
59+
metadata_parsed = json.loads(metadata_raw)
60+
# Must be a dict
61+
if isinstance(metadata_parsed, dict):
62+
# Looks good, overwrite the fallback value
63+
stringified_metadata = metadata_raw
64+
except Exception as e:
65+
logger.debug(f"Failed to parse metadata for uploaded image, {e}")
66+
pass
67+
68+
# We expect the workflow, if embedded in the image, to be a JSON-stringified WorkflowWithoutID. We will store it
69+
# as a string.
70+
workflow_raw: str | None = invokeai_workflow_override or pil_image.info.get("invokeai_workflow", None)
71+
72+
# The fallback value for workflow is None.
73+
stringified_workflow: str | None = None
74+
75+
# If the workflow is present in the image file, we will attempt to parse it as JSON. When we create images, we
76+
# always store workflows as a stringified JSON WorkflowWithoutID. So, we expect it to be a string here.
77+
if isinstance(workflow_raw, str):
78+
try:
79+
# Validate the workflow JSON before storing it
80+
WorkflowWithoutIDValidator.validate_json(workflow_raw)
81+
# Looks good, overwrite the fallback value
82+
stringified_workflow = workflow_raw
83+
except Exception:
84+
logger.debug("Failed to parse workflow for uploaded image")
85+
pass
86+
87+
# We expect the workflow, if embedded in the image, to be a JSON-stringified Graph. We will store it as a
88+
# string.
89+
graph_raw: str | None = invokeai_graph_override or pil_image.info.get("invokeai_graph", None)
90+
91+
# The fallback value for graph is None.
92+
stringified_graph: str | None = None
93+
94+
# If the graph is present in the image file, we will attempt to parse it as JSON. When we create images, we
95+
# always store graphs as a stringified JSON Graph. So, we expect it to be a string here.
96+
if isinstance(graph_raw, str):
97+
try:
98+
# Validate the graph JSON before storing it
99+
Graph.model_validate_json(graph_raw)
100+
# Looks good, overwrite the fallback value
101+
stringified_graph = graph_raw
102+
except Exception as e:
103+
logger.debug(f"Failed to parse graph for uploaded image, {e}")
104+
pass
105+
106+
return ExtractedMetadata(
107+
invokeai_metadata=stringified_metadata, invokeai_workflow=stringified_workflow, invokeai_graph=stringified_graph
108+
)

0 commit comments

Comments
 (0)