We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
`piexif.dump` errors are not safely handled
## Sentry link

<!-- The public (aka "share") Sentry issue link. -->
https://sentry.io/share/issue/a80d52de7f89436586ed0250cd0a32d2/

## Description

<!-- Example: We are trying to access property foo of ImportantClass but the instance is null. -->
<!-- Include any additional information you may have, including potential remedies if any come to mind, and the general context of the code (what causes it to run in the app).  -->
The call to `piexif.dump` should be wrapped in a `try/except` to prevent these errors in the watermark endpoint.

<!-- Mention whether this is a known regression, i.e., the feature used to work and now does not.  -->

## Reproduction

<!-- Share the steps to reproduce the issue, if you were able to, OR a note sharing that you tried to reproduce but weren’t able to. -->
Visit https://api-dev.openverse.engineering/v1/images/a913fde1-d524-4059-bd4f-9bd687578cc3/watermark/ to see an example of this failure.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `api/catalog/api/utils/watermark.py`
Content:
```
1 import logging
2 import os
3 from enum import Flag, auto
4 from io import BytesIO
5 from textwrap import wrap
6 
7 from django.conf import settings
8 
9 import piexif
10 import requests
11 from PIL import Image, ImageDraw, ImageFont
12 from sentry_sdk import capture_exception
13 
14 
15 parent_logger = logging.getLogger(__name__)
16 
17 
18 BREAKPOINT_DIMENSION = 400  # 400px
19 MARGIN_RATIO = 0.04  # 4%
20 FONT_RATIO = 0.04  # 4%
21 
22 FRAME_COLOR = "#fff"  # White frame
23 TEXT_COLOR = "#000"  # Black text
24 HEADERS = {
25     "User-Agent": settings.OUTBOUND_USER_AGENT_TEMPLATE.format(purpose="Watermark")
26 }
27 
28 
29 class Dimension(Flag):
30     """
31     This enum represents the two dimensions of an image
32     """
33 
34     HEIGHT = auto()
35     WIDTH = auto()
36     BOTH = HEIGHT | WIDTH
37     NONE = 0
38 
39 
40 # Utils
41 
42 
43 def _smaller_dimension(width, height):
44     """
45     Determine which image dimensions are below the breakpoint dimensions
46     :param width: the width of the image
47     :param height: the height of the image
48     :return: True if the image is small, False otherwise
49     """
50 
51     smaller_dimension = Dimension.NONE
52     if width < BREAKPOINT_DIMENSION:
53         smaller_dimension = smaller_dimension | Dimension.WIDTH
54     if height < BREAKPOINT_DIMENSION:
55         smaller_dimension = smaller_dimension | Dimension.HEIGHT
56     return smaller_dimension
57 
58 
59 def _get_font_path(monospace=False):
60     """
61     Return the path to the TTF font file
62     :param monospace: True for monospaced font, False for variable-width font
63     :return: the path to the TTF font file
64     """
65 
66     font_name = "SourceCodePro-Bold.ttf" if monospace else "SourceSansPro-Bold.ttf"
67     font_path = os.path.join(os.path.dirname(__file__), "fonts", font_name)
68 
69     return font_path
70 
71 
72 def _fit_in_width(text, font, max_width):
73     """
74     Break the given text so that it fits in the given space
75     :param text: the text to fit in the limited width
76     :param font: the font containing size and other info
77     :param max_width: the maximum width the text is allowed to take
78     :return: the fitted text
79     """
80 
81     char_width, _ = font.getsize("x")  # x has the closest to average width
82     max_chars = max_width // char_width
83 
84     text = "\n".join(["\n".join(wrap(line, max_chars)) for line in text.split("\n")])
85 
86     return text
87 
88 
89 # Framing
90 
91 
92 def _create_frame(dimensions):
93     """
94     Creates an frame with the given dimensions
95     :param dimensions: a tuple containing the width and height of the frame
96     :return: a white frame with the given dimensions
97     """
98 
99     return Image.new("RGB", dimensions, FRAME_COLOR)
100 
101 
102 def _frame_image(image, frame, left_margin, top_margin):
103     """
104     Fix the image in the frame with the specified spacing
105     :param image: the image to frame
106     :param frame: the frame in which to fit the image
107     :param left_margin: the margin to the left of the image
108     :param top_margin: the margin to the top of the image
109     :return: the framed image
110     """
111 
112     frame.paste(image, (left_margin, top_margin))
113     return frame
114 
115 
116 # Attribution
117 
118 
119 def _full_license(image_info):
120     """
121     Get the full license from the image info
122     :param image_info: the information about a particular image
123     :return: the full license text for the image
124     """
125 
126     license_name = image_info["license"].upper()
127     license_version = image_info["license_version"].upper()
128     prefix = "" if license_name == "CC0" else "CC "
129 
130     return f"{prefix}{license_name} {license_version}"
131 
132 
133 def _get_attribution_text(image_info):
134     """
135     Generate the attribution text from the image info
136     :param image_info: the info pertaining to the licensing of the image
137     :return: the attribution text
138     """
139 
140     title = image_info["title"]
141     creator = image_info["creator"]
142     full_license = _full_license(image_info)
143 
144     return f'"{title}" by {creator} is licensed under {full_license}.'
145 
146 
147 # Actions
148 
149 
150 def _open_image(url):
151     """
152     Read an image from a URL and convert it into a PIL Image object
153     :param url: the URL from where to read the image
154     :return: the PIL image object with the EXIF data
155     """
156     logger = parent_logger.getChild("_open_image")
157     try:
158         response = requests.get(url, headers=HEADERS)
159         img_bytes = BytesIO(response.content)
160         img = Image.open(img_bytes)
161         # Preserve EXIF metadata
162         if "exif" in img.info:
163             exif = piexif.load(img.info["exif"])
164         else:
165             exif = None
166         return img, exif
167     except requests.exceptions.RequestException as e:
168         capture_exception(e)
169         logger.error(f"Error loading image data: {e}")
170         return None, None
171 
172 
173 def _print_attribution_on_image(img, image_info):
174     """
175     Add a frame around the image and put the attribution text on the bottom
176     :param img: the image to frame and attribute
177     :param image_info: the information about a particular image
178     :return: return the framed and attributed image
179     """
180 
181     width, height = img.size
182     smaller_dimension = _smaller_dimension(width, height)
183 
184     if smaller_dimension is Dimension.NONE:
185         margin = round(MARGIN_RATIO * min(width, height))
186         font_size = round(FONT_RATIO * min(width, height))
187         new_width = width
188     else:
189         margin = round(MARGIN_RATIO * BREAKPOINT_DIMENSION)
190         font_size = round(FONT_RATIO * BREAKPOINT_DIMENSION)
191         new_width = (
192             BREAKPOINT_DIMENSION if Dimension.WIDTH in smaller_dimension else width
193         )
194 
195     font = ImageFont.truetype(_get_font_path(), size=font_size)
196 
197     text = _get_attribution_text(image_info)
198     text = _fit_in_width(text, font, new_width)
199     _, attribution_height = font.getsize_multiline(text)
200 
201     frame_width = margin + new_width + margin
202     frame_height = margin + height + margin + attribution_height + margin
203     left_margin = (frame_width - width) // 2
204 
205     frame = _create_frame(
206         (
207             frame_width,
208             frame_height,
209         )
210     )
211     _frame_image(img, frame, left_margin, margin)
212 
213     draw = ImageDraw.Draw(frame)
214     text_position_x = margin
215     text_position_y = margin + height + margin
216     draw.text(
217         xy=(
218             text_position_x,
219             text_position_y,
220         ),
221         text=text,
222         font=font,
223         fill=TEXT_COLOR,
224     )
225 
226     return frame
227 
228 
229 def watermark(image_url, info, draw_frame=True):
230     """
231     Returns a PIL Image with a watermark and embedded metadata.
232 
233     :param image_url: The URL of the image.
234     :param info: A dictionary with keys title, creator, license, and
235     license_version
236     :param draw_frame: Whether to draw an attribution frame.
237     :returns: A PIL Image and its EXIF data, if included.
238     """
239 
240     img, exif = _open_image(image_url)
241     if not draw_frame:
242         return img, exif
243     frame = _print_attribution_on_image(img, info)
244     return frame, exif
245
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/api/catalog/api/utils/watermark.py b/api/catalog/api/utils/watermark.py
--- a/api/catalog/api/utils/watermark.py
+++ b/api/catalog/api/utils/watermark.py
@@ -1,5 +1,6 @@
 import logging
 import os
+import struct
 from enum import Flag, auto
 from io import BytesIO
 from textwrap import wrap
@@ -158,17 +159,18 @@
         response = requests.get(url, headers=HEADERS)
         img_bytes = BytesIO(response.content)
         img = Image.open(img_bytes)
-        # Preserve EXIF metadata
-        if "exif" in img.info:
-            exif = piexif.load(img.info["exif"])
-        else:
-            exif = None
-        return img, exif
     except requests.exceptions.RequestException as e:
         capture_exception(e)
         logger.error(f"Error loading image data: {e}")
         return None, None
 
+    try:
+        # Preserve EXIF metadata
+        exif = piexif.load(img.info["exif"]) if "exif" in img.info else None
+        return img, exif
+    except struct.error:
+        return img, None
+
 
 def _print_attribution_on_image(img, image_info):
     """

{"golden_diff": "diff --git a/api/catalog/api/utils/watermark.py b/api/catalog/api/utils/watermark.py\n--- a/api/catalog/api/utils/watermark.py\n+++ b/api/catalog/api/utils/watermark.py\n@@ -1,5 +1,6 @@\n import logging\n import os\n+import struct\n from enum import Flag, auto\n from io import BytesIO\n from textwrap import wrap\n@@ -158,17 +159,18 @@\n         response = requests.get(url, headers=HEADERS)\n         img_bytes = BytesIO(response.content)\n         img = Image.open(img_bytes)\n-        # Preserve EXIF metadata\n-        if \"exif\" in img.info:\n-            exif = piexif.load(img.info[\"exif\"])\n-        else:\n-            exif = None\n-        return img, exif\n     except requests.exceptions.RequestException as e:\n         capture_exception(e)\n         logger.error(f\"Error loading image data: {e}\")\n         return None, None\n \n+    try:\n+        # Preserve EXIF metadata\n+        exif = piexif.load(img.info[\"exif\"]) if \"exif\" in img.info else None\n+        return img, exif\n+    except struct.error:\n+        return img, None\n+\n \n def _print_attribution_on_image(img, image_info):\n     \"\"\"\n", "issue": "`piexif.dump` errors are not safely handled\n## Sentry link\r\n\r\n<!-- The public (aka \"share\") Sentry issue link. -->\r\nhttps://sentry.io/share/issue/a80d52de7f89436586ed0250cd0a32d2/\r\n\r\n## Description\r\n\r\n<!-- Example: We are trying to access property foo of ImportantClass but the instance is null. -->\r\n<!-- Include any additional information you may have, including potential remedies if any come to mind, and the general context of the code (what causes it to run in the app).  -->\r\nThe call to `piexif.dump` should be wrapped in a `try/except` to prevent these errors in the watermark endpoint.\r\n\r\n<!-- Mention whether this is a known regression, i.e., the feature used to work and now does not.  -->\r\n\r\n## Reproduction\r\n\r\n<!-- Share the steps to reproduce the issue, if you were able to, OR a note sharing that you tried to reproduce but weren\u2019t able to. -->\r\nVisit https://api-dev.openverse.engineering/v1/images/a913fde1-d524-4059-bd4f-9bd687578cc3/watermark/ to see an example of this failure.\n", "before_files": [{"content": "import logging\nimport os\nfrom enum import Flag, auto\nfrom io import BytesIO\nfrom textwrap import wrap\n\nfrom django.conf import settings\n\nimport piexif\nimport requests\nfrom PIL import Image, ImageDraw, ImageFont\nfrom sentry_sdk import capture_exception\n\n\nparent_logger = logging.getLogger(__name__)\n\n\nBREAKPOINT_DIMENSION = 400  # 400px\nMARGIN_RATIO = 0.04  # 4%\nFONT_RATIO = 0.04  # 4%\n\nFRAME_COLOR = \"#fff\"  # White frame\nTEXT_COLOR = \"#000\"  # Black text\nHEADERS = {\n    \"User-Agent\": settings.OUTBOUND_USER_AGENT_TEMPLATE.format(purpose=\"Watermark\")\n}\n\n\nclass Dimension(Flag):\n    \"\"\"\n    This enum represents the two dimensions of an image\n    \"\"\"\n\n    HEIGHT = auto()\n    WIDTH = auto()\n    BOTH = HEIGHT | WIDTH\n    NONE = 0\n\n\n# Utils\n\n\ndef _smaller_dimension(width, height):\n    \"\"\"\n    Determine which image dimensions are below the breakpoint dimensions\n    :param width: the width of the image\n    :param height: the height of the image\n    :return: True if the image is small, False otherwise\n    \"\"\"\n\n    smaller_dimension = Dimension.NONE\n    if width < BREAKPOINT_DIMENSION:\n        smaller_dimension = smaller_dimension | Dimension.WIDTH\n    if height < BREAKPOINT_DIMENSION:\n        smaller_dimension = smaller_dimension | Dimension.HEIGHT\n    return smaller_dimension\n\n\ndef _get_font_path(monospace=False):\n    \"\"\"\n    Return the path to the TTF font file\n    :param monospace: True for monospaced font, False for variable-width font\n    :return: the path to the TTF font file\n    \"\"\"\n\n    font_name = \"SourceCodePro-Bold.ttf\" if monospace else \"SourceSansPro-Bold.ttf\"\n    font_path = os.path.join(os.path.dirname(__file__), \"fonts\", font_name)\n\n    return font_path\n\n\ndef _fit_in_width(text, font, max_width):\n    \"\"\"\n    Break the given text so that it fits in the given space\n    :param text: the text to fit in the limited width\n    :param font: the font containing size and other info\n    :param max_width: the maximum width the text is allowed to take\n    :return: the fitted text\n    \"\"\"\n\n    char_width, _ = font.getsize(\"x\")  # x has the closest to average width\n    max_chars = max_width // char_width\n\n    text = \"\\n\".join([\"\\n\".join(wrap(line, max_chars)) for line in text.split(\"\\n\")])\n\n    return text\n\n\n# Framing\n\n\ndef _create_frame(dimensions):\n    \"\"\"\n    Creates an frame with the given dimensions\n    :param dimensions: a tuple containing the width and height of the frame\n    :return: a white frame with the given dimensions\n    \"\"\"\n\n    return Image.new(\"RGB\", dimensions, FRAME_COLOR)\n\n\ndef _frame_image(image, frame, left_margin, top_margin):\n    \"\"\"\n    Fix the image in the frame with the specified spacing\n    :param image: the image to frame\n    :param frame: the frame in which to fit the image\n    :param left_margin: the margin to the left of the image\n    :param top_margin: the margin to the top of the image\n    :return: the framed image\n    \"\"\"\n\n    frame.paste(image, (left_margin, top_margin))\n    return frame\n\n\n# Attribution\n\n\ndef _full_license(image_info):\n    \"\"\"\n    Get the full license from the image info\n    :param image_info: the information about a particular image\n    :return: the full license text for the image\n    \"\"\"\n\n    license_name = image_info[\"license\"].upper()\n    license_version = image_info[\"license_version\"].upper()\n    prefix = \"\" if license_name == \"CC0\" else \"CC \"\n\n    return f\"{prefix}{license_name} {license_version}\"\n\n\ndef _get_attribution_text(image_info):\n    \"\"\"\n    Generate the attribution text from the image info\n    :param image_info: the info pertaining to the licensing of the image\n    :return: the attribution text\n    \"\"\"\n\n    title = image_info[\"title\"]\n    creator = image_info[\"creator\"]\n    full_license = _full_license(image_info)\n\n    return f'\"{title}\" by {creator} is licensed under {full_license}.'\n\n\n# Actions\n\n\ndef _open_image(url):\n    \"\"\"\n    Read an image from a URL and convert it into a PIL Image object\n    :param url: the URL from where to read the image\n    :return: the PIL image object with the EXIF data\n    \"\"\"\n    logger = parent_logger.getChild(\"_open_image\")\n    try:\n        response = requests.get(url, headers=HEADERS)\n        img_bytes = BytesIO(response.content)\n        img = Image.open(img_bytes)\n        # Preserve EXIF metadata\n        if \"exif\" in img.info:\n            exif = piexif.load(img.info[\"exif\"])\n        else:\n            exif = None\n        return img, exif\n    except requests.exceptions.RequestException as e:\n        capture_exception(e)\n        logger.error(f\"Error loading image data: {e}\")\n        return None, None\n\n\ndef _print_attribution_on_image(img, image_info):\n    \"\"\"\n    Add a frame around the image and put the attribution text on the bottom\n    :param img: the image to frame and attribute\n    :param image_info: the information about a particular image\n    :return: return the framed and attributed image\n    \"\"\"\n\n    width, height = img.size\n    smaller_dimension = _smaller_dimension(width, height)\n\n    if smaller_dimension is Dimension.NONE:\n        margin = round(MARGIN_RATIO * min(width, height))\n        font_size = round(FONT_RATIO * min(width, height))\n        new_width = width\n    else:\n        margin = round(MARGIN_RATIO * BREAKPOINT_DIMENSION)\n        font_size = round(FONT_RATIO * BREAKPOINT_DIMENSION)\n        new_width = (\n            BREAKPOINT_DIMENSION if Dimension.WIDTH in smaller_dimension else width\n        )\n\n    font = ImageFont.truetype(_get_font_path(), size=font_size)\n\n    text = _get_attribution_text(image_info)\n    text = _fit_in_width(text, font, new_width)\n    _, attribution_height = font.getsize_multiline(text)\n\n    frame_width = margin + new_width + margin\n    frame_height = margin + height + margin + attribution_height + margin\n    left_margin = (frame_width - width) // 2\n\n    frame = _create_frame(\n        (\n            frame_width,\n            frame_height,\n        )\n    )\n    _frame_image(img, frame, left_margin, margin)\n\n    draw = ImageDraw.Draw(frame)\n    text_position_x = margin\n    text_position_y = margin + height + margin\n    draw.text(\n        xy=(\n            text_position_x,\n            text_position_y,\n        ),\n        text=text,\n        font=font,\n        fill=TEXT_COLOR,\n    )\n\n    return frame\n\n\ndef watermark(image_url, info, draw_frame=True):\n    \"\"\"\n    Returns a PIL Image with a watermark and embedded metadata.\n\n    :param image_url: The URL of the image.\n    :param info: A dictionary with keys title, creator, license, and\n    license_version\n    :param draw_frame: Whether to draw an attribution frame.\n    :returns: A PIL Image and its EXIF data, if included.\n    \"\"\"\n\n    img, exif = _open_image(image_url)\n    if not draw_frame:\n        return img, exif\n    frame = _print_attribution_on_image(img, info)\n    return frame, exif\n", "path": "api/catalog/api/utils/watermark.py"}], "after_files": [{"content": "import logging\nimport os\nimport struct\nfrom enum import Flag, auto\nfrom io import BytesIO\nfrom textwrap import wrap\n\nfrom django.conf import settings\n\nimport piexif\nimport requests\nfrom PIL import Image, ImageDraw, ImageFont\nfrom sentry_sdk import capture_exception\n\n\nparent_logger = logging.getLogger(__name__)\n\n\nBREAKPOINT_DIMENSION = 400  # 400px\nMARGIN_RATIO = 0.04  # 4%\nFONT_RATIO = 0.04  # 4%\n\nFRAME_COLOR = \"#fff\"  # White frame\nTEXT_COLOR = \"#000\"  # Black text\nHEADERS = {\n    \"User-Agent\": settings.OUTBOUND_USER_AGENT_TEMPLATE.format(purpose=\"Watermark\")\n}\n\n\nclass Dimension(Flag):\n    \"\"\"\n    This enum represents the two dimensions of an image\n    \"\"\"\n\n    HEIGHT = auto()\n    WIDTH = auto()\n    BOTH = HEIGHT | WIDTH\n    NONE = 0\n\n\n# Utils\n\n\ndef _smaller_dimension(width, height):\n    \"\"\"\n    Determine which image dimensions are below the breakpoint dimensions\n    :param width: the width of the image\n    :param height: the height of the image\n    :return: True if the image is small, False otherwise\n    \"\"\"\n\n    smaller_dimension = Dimension.NONE\n    if width < BREAKPOINT_DIMENSION:\n        smaller_dimension = smaller_dimension | Dimension.WIDTH\n    if height < BREAKPOINT_DIMENSION:\n        smaller_dimension = smaller_dimension | Dimension.HEIGHT\n    return smaller_dimension\n\n\ndef _get_font_path(monospace=False):\n    \"\"\"\n    Return the path to the TTF font file\n    :param monospace: True for monospaced font, False for variable-width font\n    :return: the path to the TTF font file\n    \"\"\"\n\n    font_name = \"SourceCodePro-Bold.ttf\" if monospace else \"SourceSansPro-Bold.ttf\"\n    font_path = os.path.join(os.path.dirname(__file__), \"fonts\", font_name)\n\n    return font_path\n\n\ndef _fit_in_width(text, font, max_width):\n    \"\"\"\n    Break the given text so that it fits in the given space\n    :param text: the text to fit in the limited width\n    :param font: the font containing size and other info\n    :param max_width: the maximum width the text is allowed to take\n    :return: the fitted text\n    \"\"\"\n\n    char_width, _ = font.getsize(\"x\")  # x has the closest to average width\n    max_chars = max_width // char_width\n\n    text = \"\\n\".join([\"\\n\".join(wrap(line, max_chars)) for line in text.split(\"\\n\")])\n\n    return text\n\n\n# Framing\n\n\ndef _create_frame(dimensions):\n    \"\"\"\n    Creates an frame with the given dimensions\n    :param dimensions: a tuple containing the width and height of the frame\n    :return: a white frame with the given dimensions\n    \"\"\"\n\n    return Image.new(\"RGB\", dimensions, FRAME_COLOR)\n\n\ndef _frame_image(image, frame, left_margin, top_margin):\n    \"\"\"\n    Fix the image in the frame with the specified spacing\n    :param image: the image to frame\n    :param frame: the frame in which to fit the image\n    :param left_margin: the margin to the left of the image\n    :param top_margin: the margin to the top of the image\n    :return: the framed image\n    \"\"\"\n\n    frame.paste(image, (left_margin, top_margin))\n    return frame\n\n\n# Attribution\n\n\ndef _full_license(image_info):\n    \"\"\"\n    Get the full license from the image info\n    :param image_info: the information about a particular image\n    :return: the full license text for the image\n    \"\"\"\n\n    license_name = image_info[\"license\"].upper()\n    license_version = image_info[\"license_version\"].upper()\n    prefix = \"\" if license_name == \"CC0\" else \"CC \"\n\n    return f\"{prefix}{license_name} {license_version}\"\n\n\ndef _get_attribution_text(image_info):\n    \"\"\"\n    Generate the attribution text from the image info\n    :param image_info: the info pertaining to the licensing of the image\n    :return: the attribution text\n    \"\"\"\n\n    title = image_info[\"title\"]\n    creator = image_info[\"creator\"]\n    full_license = _full_license(image_info)\n\n    return f'\"{title}\" by {creator} is licensed under {full_license}.'\n\n\n# Actions\n\n\ndef _open_image(url):\n    \"\"\"\n    Read an image from a URL and convert it into a PIL Image object\n    :param url: the URL from where to read the image\n    :return: the PIL image object with the EXIF data\n    \"\"\"\n    logger = parent_logger.getChild(\"_open_image\")\n    try:\n        response = requests.get(url, headers=HEADERS)\n        img_bytes = BytesIO(response.content)\n        img = Image.open(img_bytes)\n    except requests.exceptions.RequestException as e:\n        capture_exception(e)\n        logger.error(f\"Error loading image data: {e}\")\n        return None, None\n\n    try:\n        # Preserve EXIF metadata\n        exif = piexif.load(img.info[\"exif\"]) if \"exif\" in img.info else None\n        return img, exif\n    except struct.error:\n        return img, None\n\n\ndef _print_attribution_on_image(img, image_info):\n    \"\"\"\n    Add a frame around the image and put the attribution text on the bottom\n    :param img: the image to frame and attribute\n    :param image_info: the information about a particular image\n    :return: return the framed and attributed image\n    \"\"\"\n\n    width, height = img.size\n    smaller_dimension = _smaller_dimension(width, height)\n\n    if smaller_dimension is Dimension.NONE:\n        margin = round(MARGIN_RATIO * min(width, height))\n        font_size = round(FONT_RATIO * min(width, height))\n        new_width = width\n    else:\n        margin = round(MARGIN_RATIO * BREAKPOINT_DIMENSION)\n        font_size = round(FONT_RATIO * BREAKPOINT_DIMENSION)\n        new_width = (\n            BREAKPOINT_DIMENSION if Dimension.WIDTH in smaller_dimension else width\n        )\n\n    font = ImageFont.truetype(_get_font_path(), size=font_size)\n\n    text = _get_attribution_text(image_info)\n    text = _fit_in_width(text, font, new_width)\n    _, attribution_height = font.getsize_multiline(text)\n\n    frame_width = margin + new_width + margin\n    frame_height = margin + height + margin + attribution_height + margin\n    left_margin = (frame_width - width) // 2\n\n    frame = _create_frame(\n        (\n            frame_width,\n            frame_height,\n        )\n    )\n    _frame_image(img, frame, left_margin, margin)\n\n    draw = ImageDraw.Draw(frame)\n    text_position_x = margin\n    text_position_y = margin + height + margin\n    draw.text(\n        xy=(\n            text_position_x,\n            text_position_y,\n        ),\n        text=text,\n        font=font,\n        fill=TEXT_COLOR,\n    )\n\n    return frame\n\n\ndef watermark(image_url, info, draw_frame=True):\n    \"\"\"\n    Returns a PIL Image with a watermark and embedded metadata.\n\n    :param image_url: The URL of the image.\n    :param info: A dictionary with keys title, creator, license, and\n    license_version\n    :param draw_frame: Whether to draw an attribution frame.\n    :returns: A PIL Image and its EXIF data, if included.\n    \"\"\"\n\n    img, exif = _open_image(image_url)\n    if not draw_frame:\n        return img, exif\n    frame = _print_attribution_on_image(img, info)\n    return frame, exif\n", "path": "api/catalog/api/utils/watermark.py"}]}