235 lines
8.1 KiB
Python
235 lines
8.1 KiB
Python
# ==========================================================
|
||
# =============== Python向Qml传输 Pixmap 图像 ===============
|
||
# ==========================================================
|
||
|
||
import os
|
||
from uuid import uuid4 # 唯一ID
|
||
from urllib.parse import unquote
|
||
from PySide2.QtCore import Qt, QByteArray, QBuffer
|
||
from PySide2.QtGui import QPixmap, QImage, QPainter, QClipboard
|
||
from PySide2.QtQuick import QQuickImageProvider
|
||
|
||
from umi_log import logger
|
||
from . import ImageQt
|
||
from ..platform import Platform
|
||
|
||
Clipboard = QClipboard() # 剪贴板
|
||
|
||
|
||
# Pixmap型图片提供器
|
||
class PixmapProviderClass(QQuickImageProvider):
|
||
def __init__(self):
|
||
super().__init__(QQuickImageProvider.Pixmap)
|
||
self.pixmapDict = {} # 缓存所有pixmap的字典
|
||
self.compDict = {} # 缓存所有组件的字典
|
||
# 空图占位符
|
||
self._noneImg = None
|
||
|
||
# 向qml返回图片,imgID不存在时返回警告图
|
||
def requestPixmap(self, path, size=None, resSize=None):
|
||
if "/" in path:
|
||
compID, imgID = path.split("/", 1)
|
||
self._delCompCache(compID, imgID) # 先清缓存
|
||
if imgID in self.pixmapDict:
|
||
self.compDict[compID] = imgID # 记录缓存
|
||
return self.pixmapDict[imgID]
|
||
else: # 清空一个组件的缓存
|
||
self._delCompCache(path)
|
||
return self._getNoneImg() # 返回占位符
|
||
|
||
# 添加一个Pixmap图片到提供器,返回imgID
|
||
def addPixmap(self, pixmap):
|
||
imgID = str(uuid4())
|
||
self.pixmapDict[imgID] = pixmap
|
||
return imgID
|
||
|
||
# 向py返回图片,相当于requestPixmap,但imgID不存在时返回None
|
||
def getPixmap(self, imgID):
|
||
return self.pixmapDict.get(imgID, None)
|
||
|
||
# 向py返回PIL对象
|
||
def getPilImage(self, imgID):
|
||
im = self.getPixmap(imgID)
|
||
if not im:
|
||
return None
|
||
try:
|
||
return ImageQt.fromqimage(im)
|
||
except Exception:
|
||
logger.error("QPixmap 转 PIL 失败。", exc_info=True, stack_info=True)
|
||
return None
|
||
|
||
# py将PIL对象写回pixmapDict。主要是记录预处理的图像
|
||
# imgID可以已存在,也可以新添加
|
||
def setPilImage(self, img, imgID=""):
|
||
try:
|
||
pixmap = ImageQt.toqpixmap(img)
|
||
except Exception as e:
|
||
logger.error("PIL 转 QPixmap 失败。", exc_info=True, stack_info=True)
|
||
return f"[Error] PIL 转 QPixmap 失败:{e}"
|
||
if not imgID:
|
||
imgID = str(uuid4())
|
||
self.pixmapDict[imgID] = pixmap
|
||
return imgID
|
||
|
||
# 从pixmapDict缓存中删除一个或一批图片
|
||
# 一般无需手动调用此函数!缓存会自动管理、清除。
|
||
def delPixmap(self, imgIDs):
|
||
if isinstance(imgIDs, str):
|
||
imgIDs = [imgIDs]
|
||
for i in imgIDs:
|
||
if i in self.pixmapDict:
|
||
del self.pixmapDict[i]
|
||
logger.debug(f"删除图片缓存,剩余:{len(self.pixmapDict)}")
|
||
|
||
# 将 QPixmap 或 QImage 转换为字节
|
||
@staticmethod
|
||
def toBytes(image):
|
||
if isinstance(image, QPixmap):
|
||
image = image.toImage()
|
||
elif not isinstance(image, QImage):
|
||
raise ValueError(
|
||
f"[Error] Only QImage or QPixmap can toBytes(), no {str(type(image))}."
|
||
)
|
||
byteArray = QByteArray() # 创建一个字节数组
|
||
buffer = QBuffer(byteArray) # 创建一个缓冲区
|
||
buffer.open(QBuffer.WriteOnly)
|
||
image.save(buffer, "PNG") # 将 QImage 保存为字节数组
|
||
buffer.close()
|
||
bytesData = byteArray.data() # 获取字节数组的内容
|
||
return bytesData
|
||
|
||
# 清空一个组件的缓存。imgID可选该组件下一次更新的图片ID。
|
||
def _delCompCache(self, compID, imgID=""):
|
||
if compID in self.compDict:
|
||
last = self.compDict[compID]
|
||
if imgID and imgID == last:
|
||
logger.warning(f"图片组件异常清理: {compID} {imgID}")
|
||
return # 如果下一次更新的ID等于当前ID,则为异常,不进行清理
|
||
if last in self.pixmapDict:
|
||
del self.pixmapDict[last]
|
||
del self.compDict[compID]
|
||
|
||
# 返回空图占位符
|
||
def _getNoneImg(self):
|
||
if self._noneImg:
|
||
return self._noneImg
|
||
pixmap = QPixmap(1, 100)
|
||
pixmap.fill(Qt.blue)
|
||
painter = QPainter(pixmap) # 绘制警告条纹
|
||
painter.setPen(Qt.red)
|
||
painter.drawLine(0, 0, 0, 5)
|
||
painter.drawLine(0, 95, 0, 100)
|
||
self._noneImg = pixmap
|
||
return self._noneImg
|
||
|
||
|
||
# 图片提供器 单例
|
||
PixmapProvider = PixmapProviderClass()
|
||
|
||
|
||
# 读入一张图片,返回该图片
|
||
# type: pixmap / qimage / error
|
||
def _imread(path):
|
||
path = unquote(path) # 做一次URL解码
|
||
if path.startswith("image://pixmapprovider/"):
|
||
path = path[23:]
|
||
if "/" in path:
|
||
compID, imgID = path.split("/", 1)
|
||
if imgID in PixmapProvider.pixmapDict:
|
||
return {"type": "pixmap", "data": PixmapProvider.pixmapDict[imgID]}
|
||
else:
|
||
return {"type": "error", "data": f"[Warning] ID not in pixmapDict: {path}"}
|
||
elif path.startswith("file:///"):
|
||
path = path[8:]
|
||
if os.path.exists(path):
|
||
try:
|
||
image = QImage(path)
|
||
return {"type": "qimage", "data": image, "path": path}
|
||
except Exception as e:
|
||
return {
|
||
"type": "error",
|
||
"data": f"[Error] QImage cannot read path: {path}",
|
||
}
|
||
else:
|
||
return {"type": "error", "data": f"[Warning] Path {path} not exists."}
|
||
elif path in PixmapProvider.pixmapDict:
|
||
return {"type": "pixmap", "data": PixmapProvider.pixmapDict[path]}
|
||
elif os.path.exists(path):
|
||
try:
|
||
image = QImage(path)
|
||
return {"type": "qimage", "data": image, "path": path}
|
||
except Exception as e:
|
||
return {"type": "error", "data": f"[Error] QImage cannot read path: {path}"}
|
||
return {"type": "error", "data": f"[Warning] Unknow: {path}"}
|
||
|
||
|
||
# 复制一张图片到剪贴板
|
||
def copyImage(path):
|
||
im = _imread(path)
|
||
typ, data = im["type"], im["data"]
|
||
if typ == "error":
|
||
return data
|
||
try:
|
||
if typ == "pixmap":
|
||
Clipboard.setPixmap(data)
|
||
elif typ == "qimage":
|
||
Clipboard.setImage(data)
|
||
return "[Success]"
|
||
except Exception as e:
|
||
return f"[Error] can't copy: {e}\n{path}"
|
||
|
||
|
||
# 用系统默认应用打开图片
|
||
def openImage(path):
|
||
im = _imread(path)
|
||
typ, data = im["type"], im["data"]
|
||
if typ == "error":
|
||
return data
|
||
# 若原本为本地图片,则直接打开
|
||
if "path" in im:
|
||
path = im["path"]
|
||
# 若为内存数据,则创建缓存文件
|
||
else:
|
||
path = "umi_temp_image.png"
|
||
try:
|
||
if typ == "pixmap":
|
||
data = data.toImage()
|
||
data.save(path)
|
||
logger.debug(f"用系统默认应用打开图片时,缓存临时图片到 {path}")
|
||
except Exception as e:
|
||
logger.error(
|
||
f"用系统默认应用打开图片时,无法缓存临时图片到 {path}",
|
||
exc_info=True,
|
||
stack_info=True,
|
||
)
|
||
return f"[Error] can't save to temp file: {e}\n{path}"
|
||
# 打开文件
|
||
try:
|
||
Platform.startfile(path)
|
||
return "[Success]"
|
||
except Exception as e:
|
||
logger.error(
|
||
f"无法用系统默认应用打开图片 {path}",
|
||
exc_info=True,
|
||
stack_info=True,
|
||
)
|
||
return f"[Error] can't open image: {e}\n{path}"
|
||
|
||
|
||
# 保存一张图片
|
||
def saveImage(fromPath, toPath):
|
||
if toPath.startswith("file:///"):
|
||
toPath = toPath[8:]
|
||
im = _imread(fromPath)
|
||
typ, data = im["type"], im["data"]
|
||
if typ == "error":
|
||
return data
|
||
try:
|
||
if typ == "pixmap":
|
||
data.save(toPath)
|
||
elif typ == "qimage":
|
||
data.save(toPath)
|
||
return f"[Success] {toPath}"
|
||
except Exception as e:
|
||
return f"[Error] can't save: {e}\n{fromPath}\n{toPath}"
|