feat
This commit is contained in:
parent
c1d05b5984
commit
4a05b50fee
50
main.ipynb
50
main.ipynb
@ -4155,6 +4155,54 @@
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "30acea7c",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"@app.get(\"/b/{bucket_name}/{file_id}/{new_w}/{quality}/{GaussianBlur_radius}\")\n",
|
||||
"async def get_file(\n",
|
||||
" bucket_name: str,\n",
|
||||
" file_id: str,\n",
|
||||
" new_w: int,\n",
|
||||
" quality: int,\n",
|
||||
" GaussianBlur_radius: int,\n",
|
||||
"):\n",
|
||||
" res = await stream_gridfs_file(file_id, bucket_name)\n",
|
||||
" if res is None:\n",
|
||||
" raise HTTPException(status_code=404, detail=\"File not found\")\n",
|
||||
"\n",
|
||||
" filename, content_type, file_length, iterator = res\n",
|
||||
"\n",
|
||||
" # Собираем все байты файла\n",
|
||||
" data = b\"\".join([chunk async for chunk in iterator()])\n",
|
||||
"\n",
|
||||
" # Обрабатываем\n",
|
||||
" processed = await build_composite(\n",
|
||||
" data,\n",
|
||||
" new_w=new_w,\n",
|
||||
" quality=quality,\n",
|
||||
" GaussianBlur_radius=GaussianBlur_radius,\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" headers = {\n",
|
||||
" \"Content-Disposition\": content_disposition_header(filename, inline=True),\n",
|
||||
" \"Cache-Control\": \"public, max-age=3600\",\n",
|
||||
" \"ETag\": f'\"{bucket_name}:{file_id}:{new_w}:{quality}:{GaussianBlur_radius}\"',\n",
|
||||
" \"Vary\": \"Accept-Encoding\",\n",
|
||||
" \"Content-Length\": str(len(processed.getvalue())),\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" return StreamingResponse(\n",
|
||||
" processed,\n",
|
||||
" media_type=\"image/webp\",\n",
|
||||
" headers=headers,\n",
|
||||
" )"
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
@ -4258,7 +4306,7 @@
|
||||
" users = [doc async for doc in users_collection.find().sort({'_id':-1})]\n",
|
||||
" return templates.TemplateResponse(\n",
|
||||
" request=request, name=\"profile/admin/users.html\", context={\"user\": user,\n",
|
||||
" 'users': users \n",
|
||||
" 'users': users\n",
|
||||
" }\n",
|
||||
" )"
|
||||
],
|
||||
|
||||
39
main.py
39
main.py
@ -3195,6 +3195,45 @@ async def get_avatar(user_id: str):
|
||||
return StreamingResponse(iterator(), media_type=content_type, headers=headers)
|
||||
|
||||
|
||||
@app.get("/b/{bucket_name}/{file_id}/{new_w}/{quality}/{GaussianBlur_radius}")
|
||||
async def get_file(
|
||||
bucket_name: str,
|
||||
file_id: str,
|
||||
new_w: int,
|
||||
quality: int,
|
||||
GaussianBlur_radius: int,
|
||||
):
|
||||
res = await stream_gridfs_file(file_id, bucket_name)
|
||||
if res is None:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
filename, content_type, file_length, iterator = res
|
||||
|
||||
# Собираем все байты файла
|
||||
data = b"".join([chunk async for chunk in iterator()])
|
||||
|
||||
# Обрабатываем
|
||||
processed = await build_composite(
|
||||
data,
|
||||
new_w=new_w,
|
||||
quality=quality,
|
||||
GaussianBlur_radius=GaussianBlur_radius,
|
||||
)
|
||||
|
||||
headers = {
|
||||
"Content-Disposition": content_disposition_header(filename, inline=True),
|
||||
"Cache-Control": "public, max-age=3600",
|
||||
"ETag": f'"{bucket_name}:{file_id}:{new_w}:{quality}:{GaussianBlur_radius}"',
|
||||
"Vary": "Accept-Encoding",
|
||||
"Content-Length": str(len(processed.getvalue())),
|
||||
}
|
||||
|
||||
return StreamingResponse(
|
||||
processed,
|
||||
media_type="image/webp",
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
async def unsubscribe_user(
|
||||
email: str,
|
||||
token: Optional[str] = None
|
||||
|
||||
55
utils.py
55
utils.py
@ -352,81 +352,72 @@ def is_safe_redirect(next_url: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
async def build_composite(data, scale: float = 1.5, new_w: int = 500, quality: int = 90, GaussianBlur_radius: int = 80):
|
||||
src_img = Image.open(BytesIO(data)).convert('RGB')
|
||||
async def build_composite(
|
||||
data: bytes,
|
||||
scale: float = 1.5,
|
||||
new_w: int = 500,
|
||||
quality: int = 90,
|
||||
GaussianBlur_radius: int = 80,
|
||||
) -> BytesIO:
|
||||
src_img = Image.open(BytesIO(data)).convert("RGB")
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# 2️⃣ Параметры конечного результата
|
||||
# --------------------------------------------------------------
|
||||
new_h = int(new_w / scale) # высота для соотношения 3:2 (≈333)
|
||||
canvas_sz = (new_w, new_h) # размер «фонового» холста
|
||||
canvas_sz = (new_w, new_h)
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# 3️⃣ Фоновое изображение – cover + blur(3)
|
||||
# --------------------------------------------------------------
|
||||
# - «cover» = заполняем весь холст, обрезая лишнее,
|
||||
# сохраняем пропорции оригинала.
|
||||
# - Затем делаем размытие радиусом 3.
|
||||
# --------------------------------------------------------------
|
||||
# Сначала масштабируем так, чтобы одна из сторон полностью покрывала холст
|
||||
src_ratio = src_img.width / src_img.height
|
||||
canvas_ratio = new_w / new_h
|
||||
|
||||
if src_ratio > canvas_ratio: # изображение более «широкое»
|
||||
# масштабируем по высоте, ширина будет больше холста → обрезаем по горизонтали
|
||||
# --------------------------------------------------------------
|
||||
# 3️⃣ Фоновое изображение – cover + blur (optional)
|
||||
# --------------------------------------------------------------
|
||||
if src_ratio > canvas_ratio:
|
||||
scale_h = new_h
|
||||
scale_w = int(scale_h * src_ratio)
|
||||
else: # изображение более «высокое» или точное соотношение
|
||||
else:
|
||||
scale_w = new_w
|
||||
scale_h = int(scale_w / src_ratio)
|
||||
|
||||
# масштабируем
|
||||
bg_resized = src_img.resize((scale_w, scale_h), Image.LANCZOS)
|
||||
|
||||
# обрезаем центр, чтобы получить точно new_w × new_h
|
||||
left = (scale_w - new_w) // 2
|
||||
top = (scale_h - new_h) // 2
|
||||
right = left + new_w
|
||||
bottom = top + new_h
|
||||
bg_cropped = bg_resized.crop((left, top, right, bottom))
|
||||
|
||||
# размытие
|
||||
bg_blurred = bg_cropped.filter(ImageFilter.GaussianBlur(radius=GaussianBlur_radius))
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# 4️⃣ Переднее изображение – contain + центрировать
|
||||
# --------------------------------------------------------------
|
||||
# - «contain» = вписать изображение в границы canvas,
|
||||
# полностью показывая его, оставляя пустые поля (прозрачные)
|
||||
# --------------------------------------------------------------
|
||||
# Находим масштаб, чтобы изображение полностью поместилось
|
||||
if src_ratio > canvas_ratio:
|
||||
# ограничиваем ширину
|
||||
fore_w = new_w
|
||||
fore_h = int(fore_w / src_ratio)
|
||||
else:
|
||||
# ограничиваем высоту
|
||||
fore_h = new_h
|
||||
fore_w = int(fore_h * src_ratio)
|
||||
|
||||
fore_resized = src_img.resize((fore_w, fore_h), Image.LANCZOS)
|
||||
|
||||
# Создаём прозрачный холст того же размера, что и фон
|
||||
final_img = Image.new('RGB', canvas_sz, (0, 0, 0))
|
||||
# --------------------------------------------------------------
|
||||
# 5️⃣ Сборка
|
||||
# --------------------------------------------------------------
|
||||
final_img = Image.new("RGB", canvas_sz, ('f', 'f', 'f'))
|
||||
|
||||
# Сначала помещаем размытый фон
|
||||
if GaussianBlur_radius:
|
||||
bg_blurred = bg_cropped.filter(ImageFilter.GaussianBlur(radius=GaussianBlur_radius))
|
||||
final_img.paste(bg_blurred, (0, 0))
|
||||
# else: оставляем чёрный фон (или можно вставить bg_cropped без blur)
|
||||
|
||||
# Затем — переднее изображение, центрированное
|
||||
offset_x = (new_w - fore_w) // 2
|
||||
offset_y = (new_h - fore_h) // 2
|
||||
final_img.paste(fore_resized, (offset_x, offset_y))
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# 5️⃣ Возврат / сохранение
|
||||
# 6️⃣ Возврат
|
||||
# --------------------------------------------------------------
|
||||
out_bytes = BytesIO()
|
||||
# final_img.save(out_bytes, format='JPEG', quality=85)
|
||||
final_img.save(out_bytes, format="WEBP", quality=quality, method=6)
|
||||
out_bytes.seek(0)
|
||||
return out_bytes # JPEG‑байты готового композитного изображения
|
||||
return out_bytes
|
||||
|
||||
Loading…
Reference in New Issue
Block a user