Fix Python 3.13 compatibility (#106)

* compat: support Python 3.13 (imghdr removal)

* compat: drop unnecessary future annotations import
This commit is contained in:
CK Zhang
2026-01-01 19:17:47 +08:00
committed by GitHub
parent 928b92ade1
commit 11d60a7487
3 changed files with 59 additions and 8 deletions
+51
View File
@@ -0,0 +1,51 @@
import io
from pathlib import Path
from PIL import Image
try:
import imghdr as _imghdr
except ModuleNotFoundError: # Python 3.13+
_imghdr = None
def what(file: str | Path | None = None, h: bytes | None = None) -> str | None:
"""
Compatibility wrapper for the removed stdlib `imghdr` module (Python 3.13+).
Behaves like `imghdr.what(file, h)` and returns a lowercase format string
like "jpeg", "png", "gif", or None if unknown.
"""
if _imghdr is not None:
return _imghdr.what(file, h)
if h is not None:
return _what_from_bytes(h)
if file is None:
return None
try:
with Image.open(str(file)) as im:
fmt = im.format
except Exception:
return None
return fmt.lower() if fmt else None
def _what_from_bytes(data: bytes) -> str | None:
if data.startswith(b"\xff\xd8\xff"):
return "jpeg"
if data.startswith(b"\x89PNG\r\n\x1a\n"):
return "png"
if data[:6] in (b"GIF87a", b"GIF89a"):
return "gif"
if data.startswith(b"BM"):
return "bmp"
if len(data) >= 12 and data.startswith(b"RIFF") and data[8:12] == b"WEBP":
return "webp"
if data.startswith(b"II*\x00") or data.startswith(b"MM\x00*"):
return "tiff"
try:
with Image.open(io.BytesIO(data)) as im:
fmt = im.format
except Exception:
return None
return fmt.lower() if fmt else None
+5 -5
View File
@@ -4,13 +4,13 @@ import logging
import io
import requests
import base64
import imghdr
from PIL import Image
import pickle
from Crypto.Cipher import AES
from .wxgf import WxgfAndroidDecoder, is_wxgf_buffer
from .parser import WeChatDBParser
from .common.imgutil import what as img_what
from .common.textutil import md5 as get_md5_hex, get_file_b64, get_file_md5
@@ -124,10 +124,10 @@ class EmojiReader:
candidates = []
def get_data_no_fallback(fname):
if imghdr.what(fname):
if img_what(fname):
data_md5 = get_file_md5(fname)
if data_md5 == md5:
return get_file_b64(fname), imghdr.what(fname)
return get_file_b64(fname), img_what(fname)
try:
content = self._decode_emoji(fname)
@@ -147,9 +147,9 @@ class EmojiReader:
logger.error(f"Error decoding emoji {fname} : {str(e)}")
def get_data_fallback(fname):
if not imghdr.what(fname):
if not img_what(fname):
return # fallback files are not encrypted
return get_file_b64(fname), imghdr.what(fname)
return get_file_b64(fname), img_what(fname)
get_data_func = get_data_fallback if allow_fallback else get_data_no_fallback
results = [(x, get_data_func(x)) for x in candidates]
+3 -3
View File
@@ -8,12 +8,12 @@ import io
import base64
import logging
logger = logging.getLogger(__name__)
import imghdr
from multiprocessing import Pool
import atexit
from .emoji import EmojiReader
from .avatar import AvatarReader
from .common.imgutil import what as img_what
from .common.textutil import md5 as get_md5_hex, get_file_b64
from .msg import TYPE_SPEAK
from .audio import parse_wechat_audio_file
@@ -167,7 +167,7 @@ class Resource(object):
# True jpeg. Simplest case.
if img_file.endswith('jpg') and \
imghdr.what(img_file) == 'jpeg':
img_what(img_file) == 'jpeg':
return get_file_b64(img_file)
if is_wxgf_file(img_file):
@@ -188,7 +188,7 @@ class Resource(object):
buf = f.read()
# File is not actually jpeg. Convert.
if imghdr.what(file=None, h=buf) != 'jpeg':
if img_what(file=None, h=buf) != 'jpeg':
try:
im = Image.open(io.BytesIO(buf))
except: