image_provider.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. # ==========================================================
  2. # =============== Python向Qml传输 Pixmap 图像 ===============
  3. # ==========================================================
  4. import os
  5. from uuid import uuid4 # 唯一ID
  6. from urllib.parse import unquote
  7. from PySide2.QtCore import Qt, QByteArray, QBuffer
  8. from PySide2.QtGui import QPixmap, QImage, QPainter, QClipboard
  9. from PySide2.QtQuick import QQuickImageProvider
  10. from umi_log import logger
  11. from . import ImageQt
  12. from ..platform import Platform
  13. Clipboard = QClipboard() # 剪贴板
  14. # Pixmap型图片提供器
  15. class PixmapProviderClass(QQuickImageProvider):
  16. def __init__(self):
  17. super().__init__(QQuickImageProvider.Pixmap)
  18. self.pixmapDict = {} # 缓存所有pixmap的字典
  19. self.compDict = {} # 缓存所有组件的字典
  20. # 空图占位符
  21. self._noneImg = None
  22. # 向qml返回图片,imgID不存在时返回警告图
  23. def requestPixmap(self, path, size=None, resSize=None):
  24. if "/" in path:
  25. compID, imgID = path.split("/", 1)
  26. self._delCompCache(compID, imgID) # 先清缓存
  27. if imgID in self.pixmapDict:
  28. self.compDict[compID] = imgID # 记录缓存
  29. return self.pixmapDict[imgID]
  30. else: # 清空一个组件的缓存
  31. self._delCompCache(path)
  32. return self._getNoneImg() # 返回占位符
  33. # 添加一个Pixmap图片到提供器,返回imgID
  34. def addPixmap(self, pixmap):
  35. imgID = str(uuid4())
  36. self.pixmapDict[imgID] = pixmap
  37. return imgID
  38. # 向py返回图片,相当于requestPixmap,但imgID不存在时返回None
  39. def getPixmap(self, imgID):
  40. return self.pixmapDict.get(imgID, None)
  41. # 向py返回PIL对象
  42. def getPilImage(self, imgID):
  43. im = self.getPixmap(imgID)
  44. if not im:
  45. return None
  46. try:
  47. return ImageQt.fromqimage(im)
  48. except Exception:
  49. logger.error("QPixmap 转 PIL 失败。", exc_info=True, stack_info=True)
  50. return None
  51. # py将PIL对象写回pixmapDict。主要是记录预处理的图像
  52. # imgID可以已存在,也可以新添加
  53. def setPilImage(self, img, imgID=""):
  54. try:
  55. pixmap = ImageQt.toqpixmap(img)
  56. except Exception as e:
  57. logger.error("PIL 转 QPixmap 失败。", exc_info=True, stack_info=True)
  58. return f"[Error] PIL 转 QPixmap 失败:{e}"
  59. if not imgID:
  60. imgID = str(uuid4())
  61. self.pixmapDict[imgID] = pixmap
  62. return imgID
  63. # 从pixmapDict缓存中删除一个或一批图片
  64. # 一般无需手动调用此函数!缓存会自动管理、清除。
  65. def delPixmap(self, imgIDs):
  66. if isinstance(imgIDs, str):
  67. imgIDs = [imgIDs]
  68. for i in imgIDs:
  69. if i in self.pixmapDict:
  70. del self.pixmapDict[i]
  71. logger.debug(f"删除图片缓存,剩余:{len(self.pixmapDict)}")
  72. # 将 QPixmap 或 QImage 转换为字节
  73. @staticmethod
  74. def toBytes(image):
  75. if isinstance(image, QPixmap):
  76. image = image.toImage()
  77. elif not isinstance(image, QImage):
  78. raise ValueError(
  79. f"[Error] Only QImage or QPixmap can toBytes(), no {str(type(image))}."
  80. )
  81. byteArray = QByteArray() # 创建一个字节数组
  82. buffer = QBuffer(byteArray) # 创建一个缓冲区
  83. buffer.open(QBuffer.WriteOnly)
  84. image.save(buffer, "PNG") # 将 QImage 保存为字节数组
  85. buffer.close()
  86. bytesData = byteArray.data() # 获取字节数组的内容
  87. return bytesData
  88. # 清空一个组件的缓存。imgID可选该组件下一次更新的图片ID。
  89. def _delCompCache(self, compID, imgID=""):
  90. if compID in self.compDict:
  91. last = self.compDict[compID]
  92. if imgID and imgID == last:
  93. logger.warning(f"图片组件异常清理: {compID} {imgID}")
  94. return # 如果下一次更新的ID等于当前ID,则为异常,不进行清理
  95. if last in self.pixmapDict:
  96. del self.pixmapDict[last]
  97. del self.compDict[compID]
  98. # 返回空图占位符
  99. def _getNoneImg(self):
  100. if self._noneImg:
  101. return self._noneImg
  102. pixmap = QPixmap(1, 100)
  103. pixmap.fill(Qt.blue)
  104. painter = QPainter(pixmap) # 绘制警告条纹
  105. painter.setPen(Qt.red)
  106. painter.drawLine(0, 0, 0, 5)
  107. painter.drawLine(0, 95, 0, 100)
  108. self._noneImg = pixmap
  109. return self._noneImg
  110. # 图片提供器 单例
  111. PixmapProvider = PixmapProviderClass()
  112. # 读入一张图片,返回该图片
  113. # type: pixmap / qimage / error
  114. def _imread(path):
  115. path = unquote(path) # 做一次URL解码
  116. if path.startswith("image://pixmapprovider/"):
  117. path = path[23:]
  118. if "/" in path:
  119. compID, imgID = path.split("/", 1)
  120. if imgID in PixmapProvider.pixmapDict:
  121. return {"type": "pixmap", "data": PixmapProvider.pixmapDict[imgID]}
  122. else:
  123. return {"type": "error", "data": f"[Warning] ID not in pixmapDict: {path}"}
  124. elif path.startswith("file:///"):
  125. path = path[8:]
  126. if os.path.exists(path):
  127. try:
  128. image = QImage(path)
  129. return {"type": "qimage", "data": image, "path": path}
  130. except Exception as e:
  131. return {
  132. "type": "error",
  133. "data": f"[Error] QImage cannot read path: {path}",
  134. }
  135. else:
  136. return {"type": "error", "data": f"[Warning] Path {path} not exists."}
  137. elif path in PixmapProvider.pixmapDict:
  138. return {"type": "pixmap", "data": PixmapProvider.pixmapDict[path]}
  139. elif os.path.exists(path):
  140. try:
  141. image = QImage(path)
  142. return {"type": "qimage", "data": image, "path": path}
  143. except Exception as e:
  144. return {"type": "error", "data": f"[Error] QImage cannot read path: {path}"}
  145. return {"type": "error", "data": f"[Warning] Unknow: {path}"}
  146. # 复制一张图片到剪贴板
  147. def copyImage(path):
  148. im = _imread(path)
  149. typ, data = im["type"], im["data"]
  150. if typ == "error":
  151. return data
  152. try:
  153. if typ == "pixmap":
  154. Clipboard.setPixmap(data)
  155. elif typ == "qimage":
  156. Clipboard.setImage(data)
  157. return "[Success]"
  158. except Exception as e:
  159. return f"[Error] can't copy: {e}\n{path}"
  160. # 用系统默认应用打开图片
  161. def openImage(path):
  162. im = _imread(path)
  163. typ, data = im["type"], im["data"]
  164. if typ == "error":
  165. return data
  166. # 若原本为本地图片,则直接打开
  167. if "path" in im:
  168. path = im["path"]
  169. # 若为内存数据,则创建缓存文件
  170. else:
  171. path = "umi_temp_image.png"
  172. try:
  173. if typ == "pixmap":
  174. data = data.toImage()
  175. data.save(path)
  176. logger.debug(f"用系统默认应用打开图片时,缓存临时图片到 {path}")
  177. except Exception as e:
  178. logger.error(
  179. f"用系统默认应用打开图片时,无法缓存临时图片到 {path}",
  180. exc_info=True,
  181. stack_info=True,
  182. )
  183. return f"[Error] can't save to temp file: {e}\n{path}"
  184. # 打开文件
  185. try:
  186. Platform.startfile(path)
  187. return "[Success]"
  188. except Exception as e:
  189. logger.error(
  190. f"无法用系统默认应用打开图片 {path}",
  191. exc_info=True,
  192. stack_info=True,
  193. )
  194. return f"[Error] can't open image: {e}\n{path}"
  195. # 保存一张图片
  196. def saveImage(fromPath, toPath):
  197. if toPath.startswith("file:///"):
  198. toPath = toPath[8:]
  199. im = _imread(fromPath)
  200. typ, data = im["type"], im["data"]
  201. if typ == "error":
  202. return data
  203. try:
  204. if typ == "pixmap":
  205. data.save(toPath)
  206. elif typ == "qimage":
  207. data.save(toPath)
  208. return f"[Success] {toPath}"
  209. except Exception as e:
  210. return f"[Error] can't save: {e}\n{fromPath}\n{toPath}"