HibiAPI / hibiapi /api /qrcode.py
DengFengLai's picture
DF.
0a1b571
raw
history blame
4.58 kB
from datetime import datetime
from enum import Enum
from io import BytesIO
from os import fdopen
from pathlib import Path
from typing import Literal, Optional, cast
from PIL import Image
from pydantic import AnyHttpUrl, BaseModel, Field, validate_arguments
from pydantic.color import Color
from qrcode import constants
from qrcode.image.pil import PilImage
from qrcode.main import QRCode
from hibiapi.utils.config import APIConfig
from hibiapi.utils.decorators import ToAsync, enum_auto_doc
from hibiapi.utils.exceptions import ClientSideException
from hibiapi.utils.net import BaseNetClient
from hibiapi.utils.routing import BaseHostUrl
from hibiapi.utils.temp import TempFile
Config = APIConfig("qrcode")
class HostUrl(BaseHostUrl):
allowed_hosts = Config["qrcode"]["icon-site"].get(list[str])
@enum_auto_doc
class QRCodeLevel(str, Enum):
"""ไบŒ็ปด็ ๅฎน้”™็Ž‡"""
LOW = "L"
"""ๆœ€ไฝŽๅฎน้”™็Ž‡"""
MEDIUM = "M"
"""ไธญ็ญ‰ๅฎน้”™็Ž‡"""
QUARTILE = "Q"
"""้ซ˜ๅฎน้”™็Ž‡"""
HIGH = "H"
"""ๆœ€้ซ˜ๅฎน้”™็Ž‡"""
@enum_auto_doc
class ReturnEncode(str, Enum):
"""ไบŒ็ปด็ ่ฟ”ๅ›ž็š„็ผ–็ ๆ–นๅผ"""
raw = "raw"
"""็›ดๆŽฅ้‡ๅฎšๅ‘ๅˆฐไบŒ็ปด็ ๅ›พ็‰‡"""
json = "json"
"""่ฟ”ๅ›žJSONๆ ผๅผ็š„ไบŒ็ปด็ ไฟกๆฏ"""
js = "js"
jsc = "jsc"
COLOR_WHITE = Color("FFFFFF")
COLOR_BLACK = Color("000000")
class QRInfo(BaseModel):
url: Optional[AnyHttpUrl] = None
path: Path
time: datetime = Field(default_factory=datetime.now)
data: str
logo: Optional[HostUrl] = None
level: QRCodeLevel = QRCodeLevel.MEDIUM
size: int = 200
code: Literal[0] = 0
status: Literal["success"] = "success"
@classmethod
@validate_arguments
async def new(
cls,
text: str,
*,
size: int = Field(
200,
gt=Config["qrcode"]["min-size"].as_number(),
lt=Config["qrcode"]["max-size"].as_number(),
),
logo: Optional[HostUrl] = None,
level: QRCodeLevel = QRCodeLevel.MEDIUM,
bgcolor: Color = COLOR_WHITE,
fgcolor: Color = COLOR_BLACK,
):
icon_stream = None
if logo is not None:
async with BaseNetClient() as client:
response = await client.get(
logo, headers={"user-agent": "HibiAPI@GitHub"}, timeout=6
)
response.raise_for_status()
icon_stream = BytesIO(response.content)
return cls(
data=text,
logo=logo,
level=level,
size=size,
path=await cls._generate(
text,
size=size,
level=level,
icon_stream=icon_stream,
bgcolor=bgcolor.as_hex(),
fgcolor=fgcolor.as_hex(),
),
)
@classmethod
@ToAsync
def _generate(
cls,
text: str,
*,
size: int = 200,
level: QRCodeLevel = QRCodeLevel.MEDIUM,
icon_stream: Optional[BytesIO] = None,
bgcolor: str = "#FFFFFF",
fgcolor: str = "#000000",
) -> Path:
qr = QRCode(
error_correction={
QRCodeLevel.LOW: constants.ERROR_CORRECT_L,
QRCodeLevel.MEDIUM: constants.ERROR_CORRECT_M,
QRCodeLevel.QUARTILE: constants.ERROR_CORRECT_Q,
QRCodeLevel.HIGH: constants.ERROR_CORRECT_H,
}[level],
border=2,
box_size=8,
)
qr.add_data(text)
image = cast(
Image.Image,
qr.make_image(
PilImage,
back_color=bgcolor,
fill_color=fgcolor,
).get_image(),
)
image = image.resize((size, size))
if icon_stream is not None:
try:
icon = Image.open(icon_stream)
except ValueError as e:
raise ClientSideException("Invalid image format.") from e
icon_width, icon_height = icon.size
image.paste(
icon,
box=(
int(size / 2 - icon_width / 2),
int(size / 2 - icon_height / 2),
int(size / 2 + icon_width / 2),
int(size / 2 + icon_height / 2),
),
mask=icon if icon.mode == "RGBA" else None,
)
descriptor, path = TempFile.create(".png")
with fdopen(descriptor, "wb") as f:
image.save(f, format="PNG")
return path