Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -91,6 +91,10 @@ MIME_MAP = {
|
|
| 91 |
"avif": "image/avif",
|
| 92 |
"heif": "image/heic",
|
| 93 |
"tiff": "image/tiff",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
}
|
| 95 |
|
| 96 |
# ---------------------------------------------------------------------------
|
|
@@ -108,12 +112,20 @@ def _compress_image(raw_bytes: bytes, quality: int, fmt: str) -> bytes:
|
|
| 108 |
img = Image.open(io.BytesIO(raw_bytes))
|
| 109 |
|
| 110 |
# Convert RGBA → RGB for formats that don't support alpha
|
| 111 |
-
if fmt in ("jpeg", "tiff") and img.mode in ("RGBA", "LA", "PA"):
|
| 112 |
background = Image.new("RGB", img.size, (255, 255, 255))
|
| 113 |
background.paste(img, mask=img.split()[-1]) # alpha composite
|
| 114 |
img = background
|
| 115 |
-
elif fmt in ("jpeg",) and img.mode
|
| 116 |
img = img.convert("RGB")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
|
| 118 |
out_buffer = io.BytesIO()
|
| 119 |
|
|
@@ -164,6 +176,63 @@ def _compress_image(raw_bytes: bytes, quality: int, fmt: str) -> bytes:
|
|
| 164 |
format="TIFF",
|
| 165 |
compression="tiff_lzw", # lossless LZW compression
|
| 166 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
else:
|
| 168 |
raise ValueError(f"Unsupported format: {fmt}")
|
| 169 |
|
|
@@ -190,7 +259,7 @@ async def root():
|
|
| 190 |
async def process_stream(
|
| 191 |
request: Request,
|
| 192 |
quality: int = Query(default=80, ge=1, le=100),
|
| 193 |
-
format: str = Query(default="jpeg", pattern="^(jpeg|png|webp|avif|heif|tiff)$"),
|
| 194 |
):
|
| 195 |
"""
|
| 196 |
Receive a raw image in the request body (Content-Type: image/*),
|
|
|
|
| 91 |
"avif": "image/avif",
|
| 92 |
"heif": "image/heic",
|
| 93 |
"tiff": "image/tiff",
|
| 94 |
+
"gif": "image/gif",
|
| 95 |
+
"bmp": "image/bmp",
|
| 96 |
+
"ico": "image/x-icon",
|
| 97 |
+
"jp2": "image/jp2",
|
| 98 |
}
|
| 99 |
|
| 100 |
# ---------------------------------------------------------------------------
|
|
|
|
| 112 |
img = Image.open(io.BytesIO(raw_bytes))
|
| 113 |
|
| 114 |
# Convert RGBA → RGB for formats that don't support alpha
|
| 115 |
+
if fmt in ("jpeg", "tiff", "bmp", "jp2") and img.mode in ("RGBA", "LA", "PA"):
|
| 116 |
background = Image.new("RGB", img.size, (255, 255, 255))
|
| 117 |
background.paste(img, mask=img.split()[-1]) # alpha composite
|
| 118 |
img = background
|
| 119 |
+
elif fmt in ("jpeg", "bmp", "jp2") and img.mode not in ("RGB", "L"):
|
| 120 |
img = img.convert("RGB")
|
| 121 |
+
elif fmt == "gif" and img.mode == "RGBA":
|
| 122 |
+
# GIF supports palette transparency; convert RGBA → P with transparency
|
| 123 |
+
img = img.convert("RGBA") # ensure consistent mode
|
| 124 |
+
alpha = img.split()[-1]
|
| 125 |
+
img = img.convert("RGB").convert("P", palette=Image.ADAPTIVE, colors=255)
|
| 126 |
+
mask = Image.eval(alpha, lambda a: 255 if a <= 128 else 0)
|
| 127 |
+
img.paste(255, mask) # map transparent pixels to index 255
|
| 128 |
+
# transparency index will be set during save
|
| 129 |
|
| 130 |
out_buffer = io.BytesIO()
|
| 131 |
|
|
|
|
| 176 |
format="TIFF",
|
| 177 |
compression="tiff_lzw", # lossless LZW compression
|
| 178 |
)
|
| 179 |
+
elif fmt == "gif":
|
| 180 |
+
# Preserve animated GIF frames if present
|
| 181 |
+
n_frames = getattr(img, 'n_frames', 1)
|
| 182 |
+
if n_frames > 1:
|
| 183 |
+
frames = []
|
| 184 |
+
durations = []
|
| 185 |
+
for i in range(n_frames):
|
| 186 |
+
img.seek(i)
|
| 187 |
+
frame = img.copy()
|
| 188 |
+
if frame.mode != "P":
|
| 189 |
+
frame = frame.convert("RGBA").convert("P", palette=Image.ADAPTIVE, colors=256)
|
| 190 |
+
frames.append(frame)
|
| 191 |
+
durations.append(img.info.get('duration', 100))
|
| 192 |
+
frames[0].save(
|
| 193 |
+
out_buffer,
|
| 194 |
+
format="GIF",
|
| 195 |
+
save_all=True,
|
| 196 |
+
append_images=frames[1:],
|
| 197 |
+
duration=durations,
|
| 198 |
+
loop=img.info.get('loop', 0),
|
| 199 |
+
optimize=True,
|
| 200 |
+
)
|
| 201 |
+
else:
|
| 202 |
+
if img.mode not in ("P", "L"):
|
| 203 |
+
img = img.convert("P", palette=Image.ADAPTIVE, colors=256)
|
| 204 |
+
img.save(
|
| 205 |
+
out_buffer,
|
| 206 |
+
format="GIF",
|
| 207 |
+
optimize=True,
|
| 208 |
+
)
|
| 209 |
+
elif fmt == "bmp":
|
| 210 |
+
if img.mode not in ("RGB", "L", "1"):
|
| 211 |
+
img = img.convert("RGB")
|
| 212 |
+
img.save(
|
| 213 |
+
out_buffer,
|
| 214 |
+
format="BMP",
|
| 215 |
+
)
|
| 216 |
+
elif fmt == "ico":
|
| 217 |
+
# ICO spec limits dimensions to 256×256
|
| 218 |
+
max_ico = 256
|
| 219 |
+
if img.width > max_ico or img.height > max_ico:
|
| 220 |
+
img.thumbnail((max_ico, max_ico), Image.LANCZOS)
|
| 221 |
+
if img.mode != "RGBA":
|
| 222 |
+
img = img.convert("RGBA")
|
| 223 |
+
img.save(
|
| 224 |
+
out_buffer,
|
| 225 |
+
format="ICO",
|
| 226 |
+
)
|
| 227 |
+
elif fmt == "jp2":
|
| 228 |
+
if img.mode not in ("RGB", "L", "RGBA"):
|
| 229 |
+
img = img.convert("RGB")
|
| 230 |
+
img.save(
|
| 231 |
+
out_buffer,
|
| 232 |
+
format="JPEG2000",
|
| 233 |
+
quality_mode="rates",
|
| 234 |
+
quality_layers=[quality],
|
| 235 |
+
)
|
| 236 |
else:
|
| 237 |
raise ValueError(f"Unsupported format: {fmt}")
|
| 238 |
|
|
|
|
| 259 |
async def process_stream(
|
| 260 |
request: Request,
|
| 261 |
quality: int = Query(default=80, ge=1, le=100),
|
| 262 |
+
format: str = Query(default="jpeg", pattern="^(jpeg|png|webp|avif|heif|tiff|gif|bmp|ico|jp2)$"),
|
| 263 |
):
|
| 264 |
"""
|
| 265 |
Receive a raw image in the request body (Content-Type: image/*),
|