diff --git a/PDF2WORD.md b/PDF2WORD.md
new file mode 100644
index 0000000000000000000000000000000000000000..382a0febee57a21ea0b77d95360feadc54ddae60
--- /dev/null
+++ b/PDF2WORD.md
@@ -0,0 +1,42 @@
+# PDF2WORD
+
+PDF2WORD是PaddleOCR社区开发者@whj 基于PP-Structure智能文档分析系统实现的PDF转换word应用程序,提供可直接安装的exe,方便windows用户运行
+
+
+
+
+
+## 1.使用
+
+### 应用程序
+
+1. 下载与安装:针对Windows用户,根据[软件下载]()一节下载软件后,运行 `pdf2word.exe` 。若您下载的是lite版本,安装过程中会在线下载环境依赖、模型等必要资源,安装时间较长,请确保网络畅通。serve版本打包了相关依赖,安装时间较短,可按需下载。
+
+2. 转换:由于PP-Structure根据中英文数据分别进行适配,在转换相应文件时可**根据文档语言进行相应选择**。
+
+### 脚本运行
+
+首次运行需要将
+
+```
+python pdf2word.py
+```
+
+
+
+## 2.自行打包
+
+PDF2WORD应用程序通过[QPT](https://github.com/QPT-Family/QPT)工具打包实现,若您修改了界面代码需要重新打包,请在 `ppstructure` 文件夹下运行下方指令
+
+```
+cd ./
+mv ./ppstructure/pdf2word .. -r
+python GenEXE.py
+```
+
+
+
+## 3.软件下载
+
+如需获取已打包程序,可以扫描下方二维码,关注公众号填写问卷后,加入PaddleOCR官方交流群免费获取20G OCR学习大礼包,内含OCR场景应用集合(包含数码管、液晶屏、车牌、高精度SVTR模型等7个垂类模型)、《动手学OCR》电子书、课程回放视频、前沿论文等重磅资料
+
diff --git a/ScreenShotWidget.py b/ScreenShotWidget.py
new file mode 100644
index 0000000000000000000000000000000000000000..87d46198c04dd9975e8adeef5a269b1ccefdd11d
--- /dev/null
+++ b/ScreenShotWidget.py
@@ -0,0 +1,1108 @@
+# coding=utf-8
+# author: fardeas
+# from: https://blog.csdn.net/u010501845/article/details/124931326
+import os
+import sys
+from datetime import datetime
+
+from qtpy import QtCore, QtGui
+from qtpy.QtWidgets import *
+from qtpy.QtCore import *
+from qtpy.QtGui import *
+
+
+class TextInputWidget(QTextEdit):
+ '''在截图区域内的文本输入框'''
+
+ def __init__(self, god=None):
+ super().__init__(god)
+ self.god = god
+ # 设置背景透明
+ # self.setStyleSheet("QTextEdit{background-color: transparent;}")
+ palette = self.palette()
+ palette.setBrush(QtGui.QPalette.ColorRole.Base, self.god.color_transparent)
+ self.setPalette(palette)
+ self.setTextColor(self.god.toolbar.curColor())
+ self.setCurrentFont(self.god.toolbar.curFont())
+ self._doc = self.document() # QTextDocument
+ self.textChanged.connect(self.adjustSizeByContent)
+ self.adjustSizeByContent() # 初始化调整高度为一行
+ self.hide()
+
+ def adjustSizeByContent(self, margin=30):
+ '''限制宽度不超出截图区域,根据文本内容调整高度,不会出现滚动条'''
+ self._doc.setTextWidth(self.viewport().width())
+ margins = self.contentsMargins()
+ h = int(self._doc.size().height() + margins.top() + margins.bottom())
+ self.setFixedHeight(h)
+
+ def beginNewInput(self, pos, endPointF):
+ '''开始新的文本输入'''
+ self._maxRect = self.god.screenArea.normalizeRectF(pos, endPointF)
+ self.waitForInput()
+
+ def waitForInput(self):
+ self.setGeometry(self._maxRect.toRect())
+ # self.setGeometry(self._maxRect.adjusted(0, 0, -1, 0)) # 宽度-1
+ self.setFocus()
+ self.show()
+
+ def loadTextInputBy(self, action):
+ '''载入修改旧的文本
+ action:(type, color, font, rectf, txt)'''
+ self.setTextColor(action[1])
+ self.setCurrentFont(action[2])
+ self._maxRect = action[3]
+ self.append(action[4])
+ self.god.isDrawing = True
+ self.waitForInput()
+
+
+class LineWidthAction(QAction):
+
+ '''画笔粗细选择器'''
+
+ def __init__(self, text, parent, lineWidth):
+ super().__init__(text, parent)
+ self._lineWidth = lineWidth
+ self.refresh(QtCore.Qt.GlobalColor.red)
+ self.triggered.connect(self.onTriggered)
+ self.setVisible(False)
+
+ def refresh(self, color):
+ painter = self.parent().god.screenArea._painter
+ dotRadius = QPointF(self._lineWidth, self._lineWidth)
+ centerPoint = self.parent().iconPixmapCenter()
+ pixmap = self.parent().iconPixmapCopy()
+ painter.begin(pixmap)
+ painter.setPen(self.parent().god.pen_transparent)
+ painter.setBrush(color)
+ painter.drawEllipse(QRectF(centerPoint - dotRadius, centerPoint + dotRadius))
+ painter.end()
+ self.setIcon(QIcon(pixmap))
+
+ def onTriggered(self):
+ self.parent()._curLineWidth = self._lineWidth
+
+
+class FontAction(QAction):
+
+ '''字体选择器'''
+
+ def __init__(self, text, parent):
+ super().__init__(text, parent)
+ self.setIcon(QIcon(r"img/sys/font.png"))
+ self._curFont = self.parent().god.font_textInput
+ self.triggered.connect(self.onTriggered)
+ self.setVisible(False)
+
+ def onTriggered(self):
+ font, ok = QFontDialog.getFont(self._curFont, self.parent(), caption='选择字体')
+ if ok:
+ self._curFont = font
+ self.parent().god.textInputWg.setCurrentFont(font)
+
+
+class ColorAction(QAction):
+
+ '''颜色选择器'''
+
+ def __init__(self, text, parent):
+ super().__init__(text, parent)
+ self._curColor = QtCore.Qt.GlobalColor.red
+ self._pixmap = QPixmap(32, 32)
+ self.refresh(self._curColor)
+ self.triggered.connect(self.onTriggered)
+
+ def refresh(self, color):
+ self._curColor = color
+ self._pixmap.fill(color)
+ self.setIcon(QIcon(self._pixmap))
+ self.parent()._at_line_small.refresh(color)
+ self.parent()._at_line_normal.refresh(color)
+ self.parent()._at_line_big.refresh(color)
+
+ def onTriggered(self):
+ col = QColorDialog.getColor(self._curColor, self.parent(), title='选择颜色')
+ if col.isValid():
+ self.refresh(col)
+ self.parent().god.textInputWg.setTextColor(col)
+
+
+class ScreenShotToolBar(QToolBar):
+ '''截图区域工具条'''
+
+ def __init__(self, god):
+ super().__init__(god)
+ self.god = god
+ self.setToolButtonStyle(QtCore.Qt.ToolButtonStyle.ToolButtonTextUnderIcon)
+ self.setStyleSheet("QToolBar {border-radius: 5px;padding: 3px;background-color: #eeeeef;}")
+ self._style_normal = "QToolBar QToolButton{color: black;}"
+ self._style_selected = "QToolBar QToolButton{color: #ff7300;border: 1px solid #BEDAF2;background-color: #D6E4F1}" # 与鼠标悬停样式一样
+ self._iconPixmap = QPixmap(32, 32)
+ self._iconPixmap.fill(self.god.color_transparent)
+ self._iconPixmapCenter = QPointF(self._iconPixmap.rect().center())
+ self._curLineWidth = 3
+ self._at_line_small = LineWidthAction('细', self, self._curLineWidth - 2)
+ self._at_line_normal = LineWidthAction('中', self, self._curLineWidth)
+ self._at_line_big = LineWidthAction('粗', self, self._curLineWidth + 2)
+ self._at_font = FontAction('字体', self)
+ self._at_color = ColorAction('颜色', self)
+ # self._at_rectangle = QAction(QIcon(r"img/sys/rectangle.png"), '矩形', self, triggered=self.beforeDrawRectangle)
+ # self._at_ellipse = QAction(QIcon(r"img/sys/ellipse.png"), '椭圆', self, triggered=self.beforeDrawEllipse)
+ # self._at_graffiti = QAction(QIcon(r"img/sys/graffiti.png"), '涂鸦', self, triggered=self.beforeDrawGraffiti)
+ # self._at_textInput = QAction(QIcon(r"img/sys/write.png"), '文字', self, triggered=self.beforeDrawText)
+ # self.addAction(self._at_line_small)
+ # self.addAction(self._at_line_normal)
+ # self.addAction(self._at_line_big)
+ # self.addAction(self._at_font)
+ # self.addAction(self._at_color)
+ # self.addSeparator()
+ # self.addAction(self._at_rectangle)
+ # self.addAction(self._at_ellipse)
+ # self.addAction(self._at_graffiti)
+ # self.addAction(self._at_textInput)
+ # self.addAction(QAction(QIcon(r"img/sys/undo.png"), '撤销', self, triggered=self.undo))
+ # self.addSeparator()
+ self.addAction(QAction(QApplication.style().standardIcon(40),
+ '退出', self, triggered=self.god.rejectSlot))
+ self.addAction(QAction(QApplication.style().standardIcon(45),
+ '接受', self, triggered=self.god.acceptSlot))
+ # self.addAction(QAction(QIcon(r"img/chat/download.png"), '保存', self, triggered=lambda: self.beforeSave('local')))
+ # self.addAction(QAction(QIcon(r"img/chat/sendImg.png"), '复制', self, triggered=lambda: self.beforeSave('clipboard')))
+ # self.actionTriggered.connect(self.onActionTriggered)
+
+ def curLineWidth(self):
+ return self._curLineWidth
+
+ def curFont(self):
+ return self._at_font._curFont
+
+ def curColor(self):
+ return self._at_color._curColor
+ # return QColor(self._at_color._curColor.toRgb()) # 颜色的副本
+
+ def iconPixmapCopy(self):
+ return self._iconPixmap.copy()
+
+ def iconPixmapCenter(self):
+ return self._iconPixmapCenter
+
+ # def onActionTriggered(self, action):
+ # '''突出显示已选中的画笔粗细、编辑模式'''
+ # for at in [self._at_line_small, self._at_line_normal, self._at_line_big]:
+ # if at._lineWidth == self._curLineWidth:
+ # self.widgetForAction(at).setStyleSheet(self._style_selected)
+ # else:
+ # self.widgetForAction(at).setStyleSheet(self._style_normal)
+ # if self.god.isDrawRectangle:
+ # self.widgetForAction(self._at_rectangle).setStyleSheet(self._style_selected)
+ # else:
+ # self.widgetForAction(self._at_rectangle).setStyleSheet(self._style_normal)
+ # if self.god.isDrawEllipse:
+ # self.widgetForAction(self._at_ellipse).setStyleSheet(self._style_selected)
+ # else:
+ # self.widgetForAction(self._at_ellipse).setStyleSheet(self._style_normal)
+ # if self.god.isDrawGraffiti:
+ # self.widgetForAction(self._at_graffiti).setStyleSheet(self._style_selected)
+ # else:
+ # self.widgetForAction(self._at_graffiti).setStyleSheet(self._style_normal)
+ # if self.god.isDrawText:
+ # self.widgetForAction(self._at_textInput).setStyleSheet(self._style_selected)
+ # else:
+ # self.widgetForAction(self._at_textInput).setStyleSheet(self._style_normal)
+
+ def setLineWidthActionVisible(self, flag):
+ self._at_line_small.setVisible(flag)
+ self._at_line_normal.setVisible(flag)
+ self._at_line_big.setVisible(flag)
+
+ def beforeDrawRectangle(self):
+ self.god.clearEditFlags()
+ self.god.isDrawRectangle = True
+ self.setLineWidthActionVisible(True)
+ self._at_font.setVisible(False)
+
+ def beforeDrawEllipse(self):
+ self.god.clearEditFlags()
+ self.god.isDrawEllipse = True
+ self.setLineWidthActionVisible(True)
+ self._at_font.setVisible(False)
+
+ def beforeDrawGraffiti(self):
+ self.god.clearEditFlags()
+ self.god.isDrawGraffiti = True
+ self.setLineWidthActionVisible(True)
+ self._at_font.setVisible(False)
+
+ def beforeDrawText(self):
+ self.god.clearEditFlags()
+ self.god.isDrawText = True
+ self.setLineWidthActionVisible(False)
+ self._at_font.setVisible(True)
+
+ def undo(self):
+ '''撤销上次编辑行为'''
+ if self.god.screenArea.undoEditAction():
+ self.god.update()
+
+ def beforeSave(self, target):
+ # 若正在编辑文本未保存,先完成编辑
+ if self.god.isDrawing and self.god.isDrawText:
+ self.god.screenArea.saveTextInputAction()
+ if target == 'local':
+ self.god.save2Local()
+ elif target == 'clipboard':
+ self.god.save2Clipboard()
+
+ def enterEvent(self, event):
+ self.god.setCursor(QtCore.Qt.CursorShape.ArrowCursor) # 工具条上显示标准箭头cursor
+
+ def leaveEvent(self, event):
+ self.god.setCursor(QtCore.Qt.CursorShape.CrossCursor) # 十字无箭头
+
+
+class ScreenArea(QtCore.QObject):
+ '''屏幕区域(提供各种算法的核心类),划分为9个子区域:
+ TopLeft,Top,TopRight
+ Left,Center,Right
+ BottomLeft,Bottom,BottomRight
+ 其中Center根据start、end两个QPointF确定
+ '''
+
+ def __init__(self, god):
+ super().__init__()
+ self.god = god
+ self._pt_start = QPointF() # 划定截图区域时鼠标左键按下的位置(topLeft)
+ self._pt_end = QPointF() # 划定截图区域时鼠标左键松开的位置(bottomRight)
+ self._rt_toolbar = QRectF() # 工具条的矩形
+ self._actions = [] # 在截图区域上的所有编辑行为(矩形、椭圆、涂鸦、文本输入等)
+ self._pt_startEdit = QPointF() # 在截图区域上绘制矩形、椭圆时鼠标左键按下的位置(topLeft)
+ self._pt_endEdit = QPointF() # 在截图区域上绘制矩形、椭圆时鼠标左键松开的位置(bottomRight)
+ self._pointfs = [] # 涂鸦经过的所有点
+ self._painter = QPainter() # 独立于ScreenShotWidget之外的画家类
+ self._textOption = QtGui.QTextOption(QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignTop)
+ self._textOption.setWrapMode(QtGui.QTextOption.WrapMode.WrapAnywhere) # 文本在矩形内自动换行
+ # self._textOption.setWrapMode(QtGui.QTextOption.WrapMode.WrapAtWordBoundaryOrAnywhere)
+ self.captureScreen()
+
+ def captureScreen(self):
+ '''抓取整个屏幕的截图'''
+ # screen = QtGui.QGuiApplication.primaryScreen()
+ self._screenPixmap = QApplication.primaryScreen().grabWindow(0)
+ self._pixelRatio = self._screenPixmap.devicePixelRatio() # 设备像素比
+ self._rt_screen = self.screenLogicalRectF()
+ self.remakeNightArea()
+
+ def normalizeRectF(self, topLeftPoint, bottomRightPoint):
+ '''根据起止点生成宽高非负数的QRectF,通常用于bottomRightPoint比topLeftPoint更左更上的情况
+ 入参可以是QPoint或QPointF'''
+ rectf = QRectF(topLeftPoint, bottomRightPoint)
+ x = rectf.x()
+ y = rectf.y()
+ w = rectf.width()
+ h = rectf.height()
+ if w < 0: # bottomRightPoint在topLeftPoint左侧时,topLeftPoint往左移动
+ x = x + w
+ w = -w
+ if h < 0: # bottomRightPoint在topLeftPoint上侧时,topLeftPoint往上移动
+ y = y + h
+ h = -h
+ return QRectF(x, y, w, h)
+
+ def physicalRectF(self, rectf):
+ '''计算划定的截图区域的(缩放倍率1.0的)原始矩形(会变大)
+ rectf:划定的截图区域的矩形。可为QRect或QRectF'''
+ return QRectF(rectf.x() * self._pixelRatio, rectf.y() * self._pixelRatio,
+ rectf.width() * self._pixelRatio, rectf.height() * self._pixelRatio)
+
+ def logicalRectF(self, physicalRectF):
+ '''根据原始矩形计算缩放后的矩形(会变小)
+ physicalRectF:缩放倍率1.0的原始矩形。可为QRect或QRectF'''
+ return QRectF(physicalRectF.x() / self._pixelRatio, physicalRectF.y() / self._pixelRatio,
+ physicalRectF.width() / self._pixelRatio, physicalRectF.height() / self._pixelRatio)
+
+ def physicalPixmap(self, rectf, editAction=False):
+ '''根据指定区域获取其原始大小的(缩放倍率1.0的)QPixmap
+ rectf:指定区域。可为QRect或QRectF
+ editAction:是否带上编辑结果'''
+ if editAction:
+ canvasPixmap = self.screenPhysicalPixmapCopy()
+ self._painter.begin(canvasPixmap)
+ self.paintEachEditAction(self._painter, textBorder=False)
+ self._painter.end()
+ return canvasPixmap.copy(self.physicalRectF(rectf).toRect())
+ else:
+ return self._screenPixmap.copy(self.physicalRectF(rectf).toRect())
+
+ def screenPhysicalRectF(self):
+ return QRectF(self._screenPixmap.rect())
+
+ def screenLogicalRectF(self):
+ return QRectF(QPointF(0, 0), self.screenLogicalSizeF()) # 即当前屏幕显示的大小
+
+ def screenPhysicalSizeF(self):
+ return QSizeF(self._screenPixmap.size())
+
+ def screenLogicalSizeF(self):
+ return QSizeF(self._screenPixmap.width() / self._pixelRatio, self._screenPixmap.height() / self._pixelRatio)
+
+ def screenPhysicalPixmapCopy(self):
+ return self._screenPixmap.copy()
+
+ def screenLogicalPixmapCopy(self):
+ return self._screenPixmap.scaled(self.screenLogicalSizeF().toSize())
+
+ def centerPhysicalRectF(self):
+ return self.physicalRectF(self._rt_center)
+
+ def centerLogicalRectF(self):
+ '''根据屏幕上的start、end两个QPointF确定'''
+ return self._rt_center
+
+ def centerPhysicalPixmap(self, editAction=True):
+ '''截图区域的QPixmap
+ editAction:是否带上编辑结果'''
+ return self.physicalPixmap(self._rt_center + QMarginsF(-1, -1, 1, 1), editAction=editAction)
+
+ def centerTopMid(self):
+ return self._pt_centerTopMid
+
+ def centerBottomMid(self):
+ return self._pt_centerBottomMid
+
+ def centerLeftMid(self):
+ return self._pt_centerLeftMid
+
+ def centerRightMid(self):
+ return self._pt_centerRightMid
+
+ def setStartPoint(self, pointf, remake=False):
+ self._pt_start = pointf
+ if remake:
+ self.remakeNightArea()
+
+ def setEndPoint(self, pointf, remake=False):
+ self._pt_end = pointf
+ if remake:
+ self.remakeNightArea()
+
+ def setCenterArea(self, start, end):
+ self._pt_start = start
+ self._pt_end = end
+ self.remakeNightArea()
+
+ def remakeNightArea(self):
+ '''重新划分九宫格区域。根据中央截图区域计算出来的其他8个区域、截图区域四个边框中点坐标等都是logical的'''
+ self._rt_center = self.normalizeRectF(self._pt_start, self._pt_end)
+ # 中央区域上下左右边框的中点,用于调整大小
+ self._pt_centerTopMid = (self._rt_center.topLeft() + self._rt_center.topRight()) / 2
+ self._pt_centerBottomMid = (self._rt_center.bottomLeft() + self._rt_center.bottomRight()) / 2
+ self._pt_centerLeftMid = (self._rt_center.topLeft() + self._rt_center.bottomLeft()) / 2
+ self._pt_centerRightMid = (self._rt_center.topRight() + self._rt_center.bottomRight()) / 2
+ # 以截图区域左上、上中、右上、左中、右中、左下、下中、右下为中心的正方形区域,用于调整大小
+ self._square_topLeft = self.squareAreaByCenter(self._rt_center.topLeft())
+ self._square_topRight = self.squareAreaByCenter(self._rt_center.topRight())
+ self._square_bottomLeft = self.squareAreaByCenter(self._rt_center.bottomLeft())
+ self._square_bottomRight = self.squareAreaByCenter(self._rt_center.bottomRight())
+ self._square_topMid = self.squareAreaByCenter(self._pt_centerTopMid)
+ self._square_bottomMid = self.squareAreaByCenter(self._pt_centerBottomMid)
+ self._square_leftMid = self.squareAreaByCenter(self._pt_centerLeftMid)
+ self._square_rightMid = self.squareAreaByCenter(self._pt_centerRightMid)
+ # 除中央截图区域外的8个区域
+ self._rt_topLeft = QRectF(self._rt_screen.topLeft(), self._rt_center.topLeft())
+ self._rt_top = QRectF(QPointF(self._rt_center.topLeft().x(), 0), self._rt_center.topRight())
+ self._rt_topRight = QRectF(QPointF(self._rt_center.topRight().x(), 0), QPointF(self._rt_screen.width(), self._rt_center.topRight().y()))
+ self._rt_left = QRectF(QPointF(0, self._rt_center.topLeft().y()), self._rt_center.bottomLeft())
+ self._rt_right = QRectF(self._rt_center.topRight(), QPointF(self._rt_screen.width(), self._rt_center.bottomRight().y()))
+ self._rt_bottomLeft = QRectF(QPointF(0, self._rt_center.bottomLeft().y()), QPointF(self._rt_center.bottomLeft().x(), self._rt_screen.height()))
+ self._rt_bottom = QRectF(self._rt_center.bottomLeft(), QPointF(self._rt_center.bottomRight().x(), self._rt_screen.height()))
+ self._rt_bottomRight = QRectF(self._rt_center.bottomRight(), self._rt_screen.bottomRight())
+
+ def squareAreaByCenter(self, pointf):
+ '''以QPointF为中心的正方形QRectF'''
+ rectf = QRectF(0, 0, 15, 15)
+ rectf.moveCenter(pointf)
+ return rectf
+
+ def aroundAreaIn8Direction(self):
+ '''中央区域周边的8个方向的区域(无交集)'''
+ return [self._rt_topLeft, self._rt_top, self._rt_topRight,
+ self._rt_left, self._rt_right,
+ self._rt_bottomLeft, self._rt_bottom, self._rt_bottomRight]
+
+ def aroundAreaIn4Direction(self):
+ '''中央区域周边的4个方向的区域(有交集)
+ 上区域(左上、上、右上):0, 0, maxX, topRight.y
+ 下区域(左下、下、右下):0, bottomLeft.y, maxX, maxY-bottomLeft.y
+ 左区域(左上、左、左下):0, 0, bottomLeft.x, maxY
+ 右区域(右上、右、右下):topRight.x, 0, maxX - topRight.x, maxY'''
+ screenSizeF = self.screenLogicalSizeF()
+ pt_topRight = self._rt_center.topRight()
+ pt_bottomLeft = self._rt_center.bottomLeft()
+ return [QRectF(0, 0, screenSizeF.width(), pt_topRight.y()),
+ QRectF(0, pt_bottomLeft.y(), screenSizeF.width(), screenSizeF.height() - pt_bottomLeft.y()),
+ QRectF(0, 0, pt_bottomLeft.x(), screenSizeF.height()),
+ QRectF(pt_topRight.x(), 0, screenSizeF.width() - pt_topRight.x(), screenSizeF.height())]
+
+ def aroundAreaWithoutIntersection(self):
+ '''中央区域周边的4个方向的区域(无交集)
+ 上区域(左上、上、右上):0, 0, maxX, topRight.y
+ 下区域(左下、下、右下):0, bottomLeft.y, maxX, maxY-bottomLeft.y
+ 左区域(左):0, topRight.y, bottomLeft.x-1, center.height
+ 右区域(右):topRight.x+1, topRight.y, maxX - topRight.x, center.height'''
+ screenSizeF = self.screenLogicalSizeF()
+ pt_topRight = self._rt_center.topRight()
+ pt_bottomLeft = self._rt_center.bottomLeft()
+ centerHeight = pt_bottomLeft.y() - pt_topRight.y()
+ return [QRectF(0, 0, screenSizeF.width(), pt_topRight.y()),
+ QRectF(0, pt_bottomLeft.y(), screenSizeF.width(), screenSizeF.height() - pt_bottomLeft.y()),
+ QRectF(0, pt_topRight.y(), pt_bottomLeft.x() - 1, centerHeight),
+ QRectF(pt_topRight.x() + 1, pt_topRight.y(), screenSizeF.width() - pt_topRight.x(), centerHeight)]
+
+ def setBeginDragPoint(self, pointf):
+ '''计算开始拖拽位置距离截图区域左上角的向量'''
+ self._drag_vector = pointf - self._rt_center.topLeft()
+
+ def getNewPosAfterDrag(self, pointf):
+ '''计算拖拽后截图区域左上角的新位置'''
+ return pointf - self._drag_vector
+
+ def moveCenterAreaTo(self, pointf):
+ '''限制拖拽不能超出屏幕范围'''
+ self._rt_center.moveTo(self.getNewPosAfterDrag(pointf))
+ startPointF = self._rt_center.topLeft()
+ if startPointF.x() < 0:
+ self._rt_center.moveTo(0, startPointF.y())
+ startPointF = self._rt_center.topLeft()
+ if startPointF.y() < 0:
+ self._rt_center.moveTo(startPointF.x(), 0)
+ screenSizeF = self.screenLogicalSizeF()
+ endPointF = self._rt_center.bottomRight()
+ if endPointF.x() > screenSizeF.width():
+ self._rt_center.moveBottomRight(QPointF(screenSizeF.width(), endPointF.y()))
+ endPointF = self._rt_center.bottomRight()
+ if endPointF.y() > screenSizeF.height():
+ self._rt_center.moveBottomRight(QPointF(endPointF.x(), screenSizeF.height()))
+ self.setCenterArea(self._rt_center.topLeft(), self._rt_center.bottomRight())
+
+ def setBeginAdjustPoint(self, pointf):
+ '''判断开始调整截图区域大小时鼠标左键在哪个区(不可能是中央区域),用于判断调整大小的意图方向'''
+ self._mousePos = self.getMousePosBy(pointf)
+
+ def getMousePosBy(self, pointf):
+ if self._square_topLeft.contains(pointf):
+ return 'TL'
+ elif self._square_topMid.contains(pointf):
+ return 'T'
+ elif self._square_topRight.contains(pointf):
+ return 'TR'
+ elif self._square_leftMid.contains(pointf):
+ return 'L'
+ elif self._rt_center.contains(pointf):
+ return 'CENTER'
+ elif self._square_rightMid.contains(pointf):
+ return 'R'
+ elif self._square_bottomLeft.contains(pointf):
+ return 'BL'
+ elif self._square_bottomMid.contains(pointf):
+ return 'B'
+ elif self._square_bottomRight.contains(pointf):
+ return 'BR'
+ else:
+ return 'ERROR'
+
+ def adjustCenterAreaBy(self, pointf):
+ '''根据开始调整截图区域大小时鼠标左键在哪个区(不可能是中央区域),判断调整大小的意图方向,判定新的开始、结束位置'''
+ startPointF = self._rt_center.topLeft()
+ endPointF = self._rt_center.bottomRight()
+ if self._mousePos == 'TL':
+ startPointF = pointf
+ elif self._mousePos == 'T':
+ startPointF = QPointF(startPointF.x(), pointf.y())
+ elif self._mousePos == 'TR':
+ startPointF = QPointF(startPointF.x(), pointf.y())
+ endPointF = QPointF(pointf.x(), endPointF.y())
+ elif self._mousePos == 'L':
+ startPointF = QPointF(pointf.x(), startPointF.y())
+ elif self._mousePos == 'R':
+ endPointF = QPointF(pointf.x(), endPointF.y())
+ elif self._mousePos == 'BL':
+ startPointF = QPointF(pointf.x(), startPointF.y())
+ endPointF = QPointF(endPointF.x(), pointf.y())
+ elif self._mousePos == 'B':
+ endPointF = QPointF(endPointF.x(), pointf.y())
+ elif self._mousePos == 'BR':
+ endPointF = pointf
+ else: # 'ERROR'
+ return
+ newRectF = self.normalizeRectF(startPointF, endPointF)
+ self.setCenterArea(newRectF.topLeft(), newRectF.bottomRight())
+
+ def getMouseShapeBy(self, pointf):
+ '''根据鼠标位置返回对应的鼠标样式'''
+ if self._rt_center.contains(pointf):
+ if self.god.isDrawRectangle or self.god.isDrawEllipse:
+ return QtCore.Qt.CursorShape.ArrowCursor
+ elif self.god.isDrawGraffiti:
+ return QtCore.Qt.CursorShape.PointingHandCursor # 超链接上的手势
+ elif self.god.isDrawText:
+ return QtCore.Qt.CursorShape.IBeamCursor # 工字
+ else:
+ return QtCore.Qt.CursorShape.SizeAllCursor # 十字有箭头
+ # return QtCore.Qt.CursorShape.OpenHandCursor # 打开的手,表示可拖拽
+ elif self._square_topLeft.contains(pointf) or self._square_bottomRight.contains(pointf):
+ return QtCore.Qt.CursorShape.SizeFDiagCursor # ↖↘
+ elif self._square_topMid.contains(pointf) or self._square_bottomMid.contains(pointf):
+ return QtCore.Qt.CursorShape.SizeVerCursor # ↑↓
+ elif self._square_topRight.contains(pointf) or self._square_bottomLeft.contains(pointf):
+ return QtCore.Qt.CursorShape.SizeBDiagCursor # ↙↗
+ elif self._square_leftMid.contains(pointf) or self._square_rightMid.contains(pointf):
+ return QtCore.Qt.CursorShape.SizeHorCursor # ←→
+ else:
+ return QtCore.Qt.CursorShape.CrossCursor # 十字无箭头
+
+ def isMousePosInCenterRectF(self, pointf):
+ return self._rt_center.contains(pointf)
+
+ def paintMagnifyingGlassPixmap(self, pos, glassSize):
+ '''绘制放大镜内的图像(含纵横十字线)
+ pos:鼠标光标位置
+ glassSize:放大镜边框大小'''
+ pixmapRect = QRect(0, 0, 20, 20) # 以鼠标光标为中心的正方形区域,最好是偶数
+ pixmapRect.moveCenter(pos)
+ glassPixmap = self.physicalPixmap(pixmapRect)
+ glassPixmap.setDevicePixelRatio(1.0)
+ glassPixmap = glassPixmap.scaled(glassSize, glassSize, QtCore.Qt.AspectRatioMode.KeepAspectRatio)
+ # 在放大后的QPixmap上画纵横十字线
+ self._painter.begin(glassPixmap)
+ halfWidth = glassPixmap.width() / 2
+ halfHeight = glassPixmap.height() / 2
+ self._painter.setPen(self.god.pen_SolidLine_lightBlue)
+ self._painter.drawLine(QPointF(0, halfHeight), QPointF(glassPixmap.width(), halfHeight))
+ self._painter.drawLine(QPointF(halfWidth, 0), QPointF(halfWidth, glassPixmap.height()))
+ self._painter.end()
+ return glassPixmap
+
+ def paintEachEditAction(self, painter, textBorder=True):
+ '''绘制所有已保存的编辑行为。编辑行为超出截图区域也无所谓,保存图像时只截取截图区域内
+ textBorder:是否绘制文本边框'''
+ for action in self.getEditActions():
+ if action[0] == 'rectangle': # (type, color, lineWidth, startPoint, endPoint)
+ self.paintRectangle(painter, action[1], action[2], action[3], action[4])
+ elif action[0] == 'ellipse': # (type, color, lineWidth, startPoint, endPoint)
+ self.paintEllipse(painter, action[1], action[2], action[3], action[4])
+ elif action[0] == 'graffiti': # (type, color, lineWidth, points)
+ self.paintGraffiti(painter, action[1], action[2], action[3])
+ elif action[0] == 'text': # (type, color, font, rectf, txt)
+ self.paintTextInput(painter, action[1], action[2], action[3], action[4], textBorder=textBorder)
+
+ def paintRectangle(self, painter, color, lineWidth, startPoint=None, endPoint=None):
+ if not startPoint:
+ startPoint = self._pt_startEdit
+ if not endPoint:
+ endPoint = self._pt_endEdit
+ qrectf = self.normalizeRectF(startPoint, endPoint)
+ if qrectf.isValid():
+ pen = QPen(color)
+ pen.setWidth(lineWidth)
+ painter.setPen(pen)
+ painter.setBrush(self.god.color_transparent)
+ painter.drawRect(qrectf)
+
+ def paintEllipse(self, painter, color, lineWidth, startPoint=None, endPoint=None):
+ if not startPoint:
+ startPoint = self._pt_startEdit
+ if not endPoint:
+ endPoint = self._pt_endEdit
+ qrectf = self.normalizeRectF(startPoint, endPoint)
+ if qrectf.isValid():
+ pen = QPen(color)
+ pen.setWidth(lineWidth)
+ painter.setPen(pen)
+ painter.setBrush(self.god.color_transparent)
+ painter.drawEllipse(qrectf)
+
+ def paintGraffiti(self, painter, color, lineWidth, pointfs=None):
+ if not pointfs:
+ pointfs = self.getGraffitiPointFs()
+ pen = QPen(color)
+ pen.setWidth(lineWidth)
+ painter.setPen(pen)
+ total = len(pointfs)
+ if total == 0:
+ return
+ elif total == 1:
+ painter.drawPoint(pointfs[0])
+ else:
+ previousPoint = pointfs[0]
+ for i in range(1, total):
+ nextPoint = pointfs[i]
+ painter.drawLine(previousPoint, nextPoint)
+ previousPoint = nextPoint
+
+ def paintTextInput(self, painter, color, font, rectf, txt, textBorder=True):
+ painter.setPen(color)
+ painter.setFont(font)
+ painter.drawText(rectf, txt, self._textOption)
+ if textBorder:
+ painter.setPen(QtCore.Qt.PenStyle.DotLine) # 点线
+ painter.setBrush(self.god.color_transparent)
+ painter.drawRect(rectf)
+
+ def getEditActions(self):
+ return self._actions.copy()
+
+ def takeTextInputActionAt(self, pointf):
+ '''根据鼠标位置查找已保存的文本输入结果,找到后取出'''
+ for i in range(len(self._actions)):
+ action = self._actions[i]
+ if action[0] == 'text' and action[3].contains(pointf):
+ return self._actions.pop(i)
+ return None
+
+ def undoEditAction(self):
+ reply = False
+ if self._actions:
+ reply = self._actions.pop()
+ if not self._actions: # 所有编辑行为都被撤销后退出编辑模式
+ self.god.exitEditMode()
+ else:
+ self.god.exitEditMode()
+ return reply
+
+ def clearEditActions(self):
+ self._actions.clear()
+
+ def setBeginEditPoint(self, pointf):
+ '''在截图区域上绘制矩形、椭圆时鼠标左键按下的位置(topLeft)'''
+ self._pt_startEdit = pointf
+ self.god.isDrawing = True
+
+ def setEndEditPoint(self, pointf):
+ '''在截图区域上绘制矩形、椭圆时鼠标左键松开的位置(bottomRight)'''
+ self._pt_endEdit = pointf
+
+ def saveRectangleAction(self):
+ rectf = self.normalizeRectF(self._pt_startEdit, self._pt_endEdit)
+ self._actions.append(('rectangle', self.god.toolbar.curColor(), self.god.toolbar.curLineWidth(),
+ rectf.topLeft(), rectf.bottomRight()))
+ self._pt_startEdit = QPointF()
+ self._pt_endEdit = QPointF()
+ self.god.isDrawing = False
+
+ def saveEllipseleAction(self):
+ rectf = self.normalizeRectF(self._pt_startEdit, self._pt_endEdit)
+ self._actions.append(('ellipse', self.god.toolbar.curColor(), self.god.toolbar.curLineWidth(),
+ rectf.topLeft(), rectf.bottomRight()))
+ self._pt_startEdit = QPointF()
+ self._pt_endEdit = QPointF()
+ self.god.isDrawing = False
+
+ def saveGraffitiPointF(self, pointf, first=False):
+ self._pointfs.append(pointf)
+ if first:
+ self.god.isDrawing = True
+
+ def getGraffitiPointFs(self):
+ return self._pointfs.copy()
+
+ def saveGraffitiAction(self):
+ if self._pointfs:
+ self._actions.append(('graffiti', self.god.toolbar.curColor(), self.god.toolbar.curLineWidth(), self._pointfs.copy()))
+ self._pointfs.clear()
+ self.god.isDrawing = False
+
+ def setBeginInputTextPoint(self, pointf):
+ '''在截图区域上输入文字时鼠标左键按下的位置(topLeft)'''
+ self.god.isDrawing = True
+ self.god.textInputWg.beginNewInput(pointf, self._pt_end)
+
+ def saveTextInputAction(self):
+ txt = self.god.textInputWg.toPlainText()
+ if txt:
+ rectf = self.god.textInputWg._maxRect # 取最大矩形的topLeft
+ rectf.setSize(QRectF(self.god.textInputWg.rect()).size()) # 取实际矩形的宽高
+ self._actions.append(('text', self.god.toolbar.curColor(), self.god.toolbar.curFont(),
+ rectf, txt))
+ self.god.textInputWg.clear()
+ self.god.textInputWg.hide() # 不管保存成功与否都取消编辑
+ self.god.isDrawing = False
+
+ def saveNightAreaImg(self):
+ '''将九宫格区域保存为本地图片,仅用于开发测试'''
+ screenPixmap = self.screenPhysicalPixmapCopy()
+ self._painter.begin(screenPixmap)
+ self._painter.setPen(self.pen_SolidLine_lightBlue)
+ self._painter.setFont(self.god.font_normal)
+ self._painter.drawRect(self._rt_center)
+ for area in self.aroundAreaIn8Direction():
+ self._painter.drawRect(area)
+ for pointf in [self._rt_center.topLeft(), self._rt_center.topRight(),
+ self._rt_center.bottomLeft(), self._rt_center.bottomRight(),
+ self._pt_centerTopMid, self._pt_centerBottomMid,
+ self._pt_centerLeftMid, self._pt_centerRightMid]:
+ self._painter.drawText(pointf + QPointF(5, -5), '(%s, %s)' % (pointf.x(), pointf.y()))
+ self._painter.end()
+ screenPixmap.save('1.jpg', quality=100)
+ self.centerPhysicalPixmap().save('2.jpg', quality=100)
+
+
+class ScreenShotWidget(QWidget):
+
+ fileType_all = '所有文件 (*);;Excel文件 (*.xls *.xlsx);;图片文件 (*.jpg *.jpeg *.gif *.png *.bmp)'
+ fileType_img = '图片文件 (*.jpg *.jpeg *.gif *.png *.bmp)'
+ dir_lastAccess = os.getcwd() # 最后访问目录
+
+ def __init__(self):
+ super().__init__()
+ self.setMouseTracking(True)
+ self.setWindowFlags(QtCore.Qt.WindowType.FramelessWindowHint | QtCore.Qt.WindowType.WindowStaysOnTopHint)
+ self.initPainterTool()
+ self.initFunctionalFlag()
+ self.screenArea = ScreenArea(self)
+ self.toolbar = ScreenShotToolBar(self)
+ self.textInputWg = TextInputWidget(self)
+
+ self.captureImage = None
+ # 设置 screenPixmap 为窗口背景
+ # palette = QtGui.QPalette()
+ # palette.setBrush(QtGui.QPalette.ColorRole.Window, QtGui.QBrush(self.screenArea.screenPhysicalPixmapCopy()))
+ # self.setPalette(palette)
+
+ def rejectSlot(self):
+ self.captureImage = None
+ self.close()
+
+ def acceptSlot(self):
+ self.captureImage = self.screenArea.centerPhysicalPixmap().toImage()
+ self.close()
+
+ def start(self):
+ self.screenArea.captureScreen()
+ self.setGeometry(self.screenArea.screenPhysicalRectF().toRect())
+ self.clearScreenShotArea()
+ self.showFullScreen()
+
+ def initPainterTool(self):
+ self.painter = QPainter()
+ self.color_transparent = QtCore.Qt.GlobalColor.transparent
+ self.color_black = QColor(0, 0, 0, 64) # 黑色背景
+ self.color_lightBlue = QColor(30, 120, 255) # 浅蓝色。深蓝色QtCore.Qt.GlobalColor.blue
+ self.font_normal = QtGui.QFont('Times New Roman', 11, QtGui.QFont.Weight.Normal)
+ self.font_textInput = QtGui.QFont('微软雅黑', 16, QtGui.QFont.Weight.Normal) # 工具条文字工具默认字体
+ self.pen_transparent = QPen(QtCore.Qt.PenStyle.NoPen) # 没有笔迹,画不出线条
+ self.pen_white = QPen(QtCore.Qt.GlobalColor.white)
+ self.pen_SolidLine_lightBlue = QPen(self.color_lightBlue) # 实线,浅蓝色
+ self.pen_SolidLine_lightBlue.setStyle(QtCore.Qt.PenStyle.DashLine) # 实线SolidLine,虚线DashLine,点线DotLine
+ self.pen_SolidLine_lightBlue.setWidthF(0) # 0表示线宽为1
+ self.pen_DashLine_lightBlue = QPen(self.color_lightBlue) # 虚线,浅蓝色
+ self.pen_DashLine_lightBlue.setStyle(QtCore.Qt.PenStyle.DashLine)
+
+ def initFunctionalFlag(self):
+ self.hasScreenShot = False # 是否已通过拖动鼠标左键划定截图区域
+ self.isCapturing = False # 正在拖动鼠标左键选定截图区域时
+ self.isMoving = False # 在截图区域内拖动时
+ self.isAdjusting = False # 在截图区域的边框按住鼠标左键调整大小时
+ self.isDrawing = False # 是否已在截图区域内开始绘制
+ self.isDrawRectangle = False # 正在截图区域内画矩形
+ self.isDrawEllipse = False # 正在截图区域内画椭圆
+ self.isDrawGraffiti = False # 正在截图区域内进行涂鸦
+ self.isDrawText = False # 正在截图区域内画文字
+ self.setCursor(QtCore.Qt.CursorShape.CrossCursor) # 设置鼠标样式 十字
+
+ def paintEvent(self, event):
+ centerRectF = self.screenArea.centerLogicalRectF()
+ screenSizeF = self.screenArea.screenLogicalSizeF()
+ canvasPixmap = self.screenArea.screenPhysicalPixmapCopy()
+ # canvasPixmap = QPixmap(screenSizeF.toSize())
+ # canvasPixmap.fill(self.color_transparent)
+ # 在屏幕截图的副本上绘制已选定的截图区域
+ self.painter.begin(canvasPixmap)
+ if self.hasScreenShot:
+ self.paintCenterArea(centerRectF) # 绘制中央截图区域
+ self.paintMaskLayer(screenSizeF, fullScreen=False) # 绘制截图区域的周边区域遮罩层
+ else:
+ self.paintMaskLayer(screenSizeF)
+ self.paintMagnifyingGlass(screenSizeF) # 在鼠标光标右下角显示放大镜
+ self.paintToolbar(centerRectF, screenSizeF) # 在截图区域右下角显示工具条
+ self.paintEditActions() # 在截图区域绘制编辑行为结果
+ self.painter.end()
+ # 把画好的绘制结果显示到窗口上
+ self.painter.begin(self)
+ self.painter.drawPixmap(0, 0, canvasPixmap) # 从坐标(0, 0)开始绘制
+ self.painter.end()
+
+ def paintCenterArea(self, centerRectF):
+ '''绘制已选定的截图区域'''
+ self.painter.setRenderHint(QPainter.RenderHint.Antialiasing, True) # 反走样
+ # 1.绘制矩形线框
+ self.painter.setPen(self.pen_DashLine_lightBlue)
+ self.painter.drawRect(centerRectF)
+ # 2.绘制矩形线框4个端点和4条边框的中间点
+ if centerRectF.width() >= 100 and centerRectF.height() >= 100:
+ points = [ # 点坐标
+ centerRectF.topLeft(), centerRectF.topRight(), centerRectF.bottomLeft(), centerRectF.bottomRight(),
+ self.screenArea.centerLeftMid(), self.screenArea.centerRightMid(),
+ self.screenArea.centerTopMid(), self.screenArea.centerBottomMid()
+ ]
+ blueDotRadius = QPointF(2, 2) # 椭圆蓝点
+ self.painter.setBrush(self.color_lightBlue)
+ for point in points:
+ self.painter.drawEllipse(QRectF(point - blueDotRadius, point + blueDotRadius))
+ # 3.在截图区域左上角显示截图区域宽高
+ if centerRectF.topLeft().y() > 20:
+ labelPos = centerRectF.topLeft() + QPointF(5, -5)
+ else: # 拖拽截图区域到贴近屏幕上边缘时“宽x高”移动到截图区域左上角的下侧
+ labelPos = centerRectF.topLeft() + QPointF(5, 15)
+ centerPhysicalRect = self.screenArea.centerPhysicalRectF().toRect()
+ self.painter.setPen(self.pen_white)
+ self.painter.setFont(self.font_normal)
+ self.painter.drawText(labelPos, '%s x %s' % (centerPhysicalRect.width(), centerPhysicalRect.height()))
+ # 4.在屏幕左上角预览截图结果
+ # self.painter.drawPixmap(0, 0, self.screenArea.centerPhysicalPixmap()) # 从坐标(0, 0)开始绘制
+
+ def paintMaskLayer(self, screenSizeF, fullScreen=True):
+ if fullScreen: # 全屏遮罩层
+ maskPixmap = QPixmap(screenSizeF.toSize())
+ maskPixmap.fill(self.color_black)
+ self.painter.drawPixmap(0, 0, maskPixmap)
+ else: # 绘制截图区域的周边区域遮罩层,以凸显截图区域
+ # 方法一:截图区域以外的8个方向区域
+ # for area in self.screenArea.aroundAreaIn8Direction():
+ # area = area.normalized()
+ # maskPixmap = QPixmap(area.size().toSize()) # 由于float转int的精度问题,可能会存在黑线条缝隙
+ # maskPixmap.fill(self.color_black)
+ # self.painter.drawPixmap(area.topLeft(), maskPixmap)
+ # 方法二:截图区域以外的上下左右区域(有交集,交集部分颜色加深,有明显的纵横效果)
+ # for area in self.screenArea.aroundAreaIn4Direction():
+ # maskPixmap = QPixmap(area.size().toSize())
+ # maskPixmap.fill(self.color_black)
+ # self.painter.drawPixmap(area.topLeft(), maskPixmap)
+ # 方法三:截图区域以外的上下左右区域(无交集)
+ for area in self.screenArea.aroundAreaWithoutIntersection():
+ maskPixmap = QPixmap(area.size().toSize())
+ maskPixmap.fill(self.color_black)
+ self.painter.drawPixmap(area.topLeft(), maskPixmap)
+
+ def paintMagnifyingGlass(self, screenSizeF, glassSize=150, offset=30, labelHeight=30):
+ '''未划定截图区域模式时、正在划定截取区域时、调整截取区域大小时在鼠标光标右下角显示放大镜
+ glassSize:放大镜正方形边长
+ offset:放大镜任意一个端点距离鼠标光标位置的最近距离
+ labelHeight:pos和rgb两行文字的高度'''
+ if self.hasScreenShot and (not self.isCapturing) and (not self.isAdjusting):
+ return
+ pos = QtGui.QCursor.pos()
+ glassPixmap = self.screenArea.paintMagnifyingGlassPixmap(pos, glassSize) # 画好纵横十字线后的放大镜内QPixmap
+ # 限制放大镜显示不超出屏幕外
+ glassRect = glassPixmap.rect()
+ if (pos.x() + glassSize + offset) < screenSizeF.width():
+ if (pos.y() + offset + glassSize + labelHeight) < screenSizeF.height():
+ glassRect.moveTo(pos + QPoint(offset, offset))
+ else:
+ glassRect.moveBottomLeft(pos + QPoint(offset, -offset))
+ else:
+ if (pos.y() + offset + glassSize + labelHeight) < screenSizeF.height():
+ glassRect.moveTopRight(pos + QPoint(-offset, offset))
+ else:
+ glassRect.moveBottomRight(pos + QPoint(-offset, -offset))
+ self.painter.drawPixmap(glassRect.topLeft(), glassPixmap)
+ # 显示pos:(x, y)、rgb:(255,255,255)
+ qrgb = QtGui.QRgba64.fromArgb32(glassPixmap.toImage().pixel(glassPixmap.rect().center()))
+ labelRectF = QRectF(glassRect.bottomLeft().x(), glassRect.bottomLeft().y(), glassSize, labelHeight)
+ self.painter.setPen(self.pen_transparent)
+ self.painter.setBrush(self.color_black) # 黑底
+ self.painter.drawRect(labelRectF)
+ self.painter.setPen(self.pen_white)
+ self.painter.setFont(self.font_normal)
+ self.painter.drawText(labelRectF,
+ QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignVCenter,
+ 'pos:(%s, %s)\nrgb:(%s, %s, %s)' % (pos.x(), pos.y(), qrgb.red8(), qrgb.green8(), qrgb.blue8()))
+
+ def paintToolbar(self, centerRectF, screenSizeF):
+ '''在截图区域右下角显示工具条'''
+ if self.hasScreenShot:
+ if self.isCapturing or self.isAdjusting:
+ self.toolbar.hide() # 正在划定截取区域时、调整截图区域大小时不显示工具条
+ else:
+ self.toolbar.adjustSize()
+ toolbarRectF = QRectF(self.toolbar.rect())
+ # 工具条位置优先顺序:右下角下侧,右上角上侧,右下角上侧
+ if (screenSizeF.height() - centerRectF.bottomRight().y()) > toolbarRectF.height():
+ toolbarRectF.moveTopRight(centerRectF.bottomRight() + QPointF(-5, 5))
+ elif centerRectF.topRight().y() > toolbarRectF.height():
+ toolbarRectF.moveBottomRight(centerRectF.topRight() + QPointF(-5, -5))
+ else:
+ toolbarRectF.moveBottomRight(centerRectF.bottomRight() + QPointF(-5, -5))
+ # 限制工具条的x坐标不为负数,不能移出屏幕外
+ if toolbarRectF.x() < 0:
+ pos = toolbarRectF.topLeft()
+ pos.setX(centerRectF.x() + 5)
+ toolbarRectF.moveTo(pos)
+ self.toolbar.move(toolbarRectF.topLeft().toPoint())
+ self.toolbar.show()
+ else:
+ self.toolbar.hide()
+
+ def paintEditActions(self):
+ '''在截图区域绘制编辑行为结果。编辑行为超出截图区域也无所谓,保存图像时只截取截图区域内'''
+ # 1.绘制正在拖拽编辑中的矩形、椭圆、涂鸦
+ if self.isDrawRectangle:
+ self.screenArea.paintRectangle(self.painter, self.toolbar.curColor(), self.toolbar.curLineWidth())
+ elif self.isDrawEllipse:
+ self.screenArea.paintEllipse(self.painter, self.toolbar.curColor(), self.toolbar.curLineWidth())
+ elif self.isDrawGraffiti:
+ self.screenArea.paintGraffiti(self.painter, self.toolbar.curColor(), self.toolbar.curLineWidth())
+ # 2.绘制所有已保存的编辑行为
+ self.screenArea.paintEachEditAction(self.painter)
+
+ def clearEditFlags(self):
+ self.isDrawing = False
+ self.isDrawRectangle = False
+ self.isDrawEllipse = False
+ self.isDrawGraffiti = False
+ self.isDrawText = False
+
+ def exitEditMode(self):
+ '''退出编辑模式'''
+ self.clearEditFlags()
+ # self.toolbar.onActionTriggered(None) # 清空工具条工具按钮选中状态
+ self.textInputWg.hide()
+
+ def clearScreenShotArea(self):
+ '''清空已划定的截取区域'''
+ self.screenArea.clearEditActions() # 清除已保存的编辑行为
+ self.exitEditMode()
+ self.hasScreenShot = False
+ self.isCapturing = False
+ pos = QPointF()
+ self.screenArea.setCenterArea(pos, pos)
+ self.update()
+ self.setCursor(QtCore.Qt.CursorShape.CrossCursor) # 设置鼠标样式 十字
+
+ def mousePressEvent(self, event):
+ if event.button() == QtCore.Qt.MouseButton.LeftButton:
+ pos = event.pos()
+ if self.hasScreenShot:
+ if self.isDrawRectangle or self.isDrawEllipse:
+ self.screenArea.setBeginEditPoint(pos)
+ elif self.isDrawGraffiti: # 保存涂鸦经过的每一个点
+ self.screenArea.saveGraffitiPointF(pos, first=True)
+ elif self.isDrawText:
+ if self.isDrawing:
+ if QRectF(self.textInputWg.rect()).contains(pos):
+ pass # 在输入框内调整光标位置,忽略
+ else: # 鼠标点到输入框之外,完成编辑
+ self.screenArea.saveTextInputAction()
+ else: # 未开始编辑时(暂不支持文本拖拽)
+ action = self.screenArea.takeTextInputActionAt(pos)
+ if action: # 鼠标点到输入框之内,修改旧的文本输入
+ self.textInputWg.loadTextInputBy(action)
+ else: # 鼠标点到输入框之外,开始新的文本输入
+ self.screenArea.setBeginInputTextPoint(pos)
+ elif self.screenArea.isMousePosInCenterRectF(pos):
+ self.isMoving = True # 进入拖拽移动模式
+ self.screenArea.setBeginDragPoint(pos)
+ else:
+ self.isAdjusting = True # 进入调整大小模式
+ self.screenArea.setBeginAdjustPoint(pos)
+ else:
+ self.screenArea.setCenterArea(pos, pos)
+ self.isCapturing = True # 进入划定截图区域模式
+ if event.button() == QtCore.Qt.MouseButton.RightButton:
+ if self.hasScreenShot or self.isCapturing: # 清空已划定的的截图区域
+ self.clearScreenShotArea()
+ else:
+ self.close()
+
+ def mouseReleaseEvent(self, event):
+ if event.button() == QtCore.Qt.MouseButton.LeftButton:
+ if self.isDrawRectangle:
+ self.screenArea.saveRectangleAction()
+ elif self.isDrawEllipse:
+ self.screenArea.saveEllipseleAction()
+ elif self.isDrawGraffiti:
+ self.screenArea.saveGraffitiAction()
+ self.isCapturing = False
+ self.isMoving = False
+ self.isAdjusting = False
+ self.toolbar.show()
+
+ def mouseMoveEvent(self, event):
+ pos = event.pos()
+ if self.isDrawing:
+ if self.isDrawRectangle or self.isDrawEllipse:
+ self.screenArea.setEndEditPoint(pos)
+ elif self.isDrawGraffiti:
+ self.screenArea.saveGraffitiPointF(pos)
+ elif self.isCapturing:
+ self.hasScreenShot = True
+ self.screenArea.setEndPoint(pos, remake=True)
+ elif self.isMoving:
+ self.screenArea.moveCenterAreaTo(pos)
+ elif self.isAdjusting:
+ self.screenArea.adjustCenterAreaBy(pos)
+ self.update()
+ if self.hasScreenShot:
+ self.setCursor(self.screenArea.getMouseShapeBy(pos))
+ else:
+ self.setCursor(QtCore.Qt.CursorShape.CrossCursor) # 设置鼠标样式 十字
+
+ def mouseDoubleClickEvent(self, event):
+ if event.button() == QtCore.Qt.MouseButton.LeftButton:
+ if self.screenArea.isMousePosInCenterRectF(event.pos()):
+ self.save2Clipboard()
+ self.close()
+
+ def keyPressEvent(self, QKeyEvent):
+ if QKeyEvent.key() == QtCore.Qt.Key.Key_Escape:
+ self.close()
+ if QKeyEvent.key() in (QtCore.Qt.Key.Key_Return, QtCore.Qt.Key.Key_Enter): # 大键盘、小键盘回车
+ self.save2Clipboard()
+ self.close()
+
+ def save2Clipboard(self):
+ '''将截图区域复制到剪贴板'''
+ if self.hasScreenShot:
+ mimData = QtCore.QMimeData()
+ mimData.setImageData(self.screenArea.centerPhysicalPixmap().toImage())
+ QApplication.clipboard().setMimeData(mimData)
+ # self.screenArea.saveNightAreaImg()
+ self.close()
+
+ def save2Local(self):
+ fileType = self.fileType_img
+ filePath, fileFormat = self.sys_selectSaveFilePath(self, fileType=fileType)
+ if filePath:
+ self.screenArea.centerPhysicalPixmap().save(filePath, quality=100)
+ self.close()
+
+ def sys_getCurTime(self, fmt='%Y-%m-%d %H:%M:%S'):
+ '''获取字符串格式的当前时间'''
+ # return QtCore.QDateTime.currentDateTime().toString('yyyy-MM-dd hh:mm:ss')
+ return datetime.now().strftime(fmt)
+
+ def sys_selectSaveFilePath(self, widget, title='选择文件保存路径', saveFileDir=None,
+ saveFileName='', defaultFileFmt='%Y%m%d%H%M%S', fileType=None):
+ '''选择文件保存路径
+ title:选择窗口标题
+ saveFileDir:指定保存目录
+ saveFileName:默认保存文件名
+ defaultFileFmt:不指定saveFileName时,自动以此格式的时间字符串命名文件
+ fileType:可以选择的文件类型
+ return:(所选的文件保存路径, 文件的类型)
+ '''
+ options = QFileDialog.Option.ReadOnly
+ if saveFileName == '':
+ saveFileName = self.sys_getCurTime(defaultFileFmt)
+ if not saveFileDir:
+ saveFileDir = self.dir_lastAccess
+ saveFilePath = os.path.join(saveFileDir, saveFileName)
+ if not fileType:
+ fileType = self.fileType_all
+ filePath, fileFormat = QFileDialog.getSaveFileName(widget, title, saveFilePath, fileType, options=options)
+ if filePath:
+ self.dir_lastAccess = os.path.dirname(filePath)
+ return (filePath, fileFormat)
+
+
diff --git a/icons/chinese.png b/icons/chinese.png
new file mode 100644
index 0000000000000000000000000000000000000000..328e2fff73bd75188fa888aae45c8cb4ca844f57
Binary files /dev/null and b/icons/chinese.png differ
diff --git a/icons/english.png b/icons/english.png
new file mode 100644
index 0000000000000000000000000000000000000000..536c4a910ae6fc05040f4958477a34aeae891ea0
Binary files /dev/null and b/icons/english.png differ
diff --git a/icons/folder-open.png b/icons/folder-open.png
new file mode 100644
index 0000000000000000000000000000000000000000..ab5f55f5a4819add116113b55f717f7a21aeafdd
Binary files /dev/null and b/icons/folder-open.png differ
diff --git a/icons/folder-plus.png b/icons/folder-plus.png
new file mode 100644
index 0000000000000000000000000000000000000000..01ce6c10b0ed3e3975edbebbc8e886f846fabe8d
Binary files /dev/null and b/icons/folder-plus.png differ
diff --git a/image2doc.py b/image2doc.py
new file mode 100644
index 0000000000000000000000000000000000000000..700126e0249cdbf7df415f7e8a07931b1241c4bf
--- /dev/null
+++ b/image2doc.py
@@ -0,0 +1,349 @@
+import sys
+import tarfile
+import os
+import time
+import functools
+import cv2
+import platform
+import numpy as np
+from qtpy import QtWidgets
+from qtpy.QtGui import QImage, QPixmap, QIcon
+
+from ppstructure.predict_system import StructureSystem, save_structure_res
+from ppstructure.utility import parse_args, draw_structure_result
+from ppocr.utils.network import download_with_progressbar
+from ppstructure.recovery.recovery_to_doc import sorted_layout_boxes, convert_info_docx
+from ScreenShotWidget import ScreenShotWidget
+
+__APPNAME__ = "Image2Doc"
+__VERSION__ = "0.0.2"
+here = os.path.dirname(os.path.abspath(__file__))
+URLs_EN = {
+ # 下载超英文轻量级PP-OCRv3模型的检测模型并解压
+ "en_PP-OCRv3_det_infer": "https://paddleocr.bj.bcebos.com/PP-OCRv3/english/en_PP-OCRv3_det_infer.tar",
+ # 下载英文轻量级PP-OCRv3模型的识别模型并解压
+ "en_PP-OCRv3_rec_infer": "https://paddleocr.bj.bcebos.com/PP-OCRv3/english/en_PP-OCRv3_rec_infer.tar",
+ # 下载超轻量级英文表格英文模型并解压
+ "en_ppstructure_mobile_v2.0_SLANet_infer": "https://paddleocr.bj.bcebos.com/ppstructure/models/slanet/en_ppstructure_mobile_v2.0_SLANet_infer.tar",
+ # 英文版面分析模型
+ "picodet_lcnet_x1_0_fgd_layout_infer": "https://paddleocr.bj.bcebos.com/ppstructure/models/layout/picodet_lcnet_x1_0_fgd_layout_infer.tar",
+}
+DICT_EN = {
+ "rec_char_dict_path": "en_dict.txt",
+ "layout_dict_path": "layout_publaynet_dict.txt",
+}
+
+URLs_CN = {
+ # 下载超中文轻量级PP-OCRv3模型的检测模型并解压
+ "cn_PP-OCRv3_det_infer": "https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_det_infer.tar",
+ # 下载中文轻量级PP-OCRv3模型的识别模型并解压
+ "cn_PP-OCRv3_rec_infer": "https://paddleocr.bj.bcebos.com/PP-OCRv3/chinese/ch_PP-OCRv3_rec_infer.tar",
+ # 下载超轻量级英文表格英文模型并解压
+ "cn_ppstructure_mobile_v2.0_SLANet_infer": "https://paddleocr.bj.bcebos.com/ppstructure/models/slanet/en_ppstructure_mobile_v2.0_SLANet_infer.tar",
+ # 中文版面分析模型
+ "picodet_lcnet_x1_0_fgd_layout_cdla_infer": "https://paddleocr.bj.bcebos.com/ppstructure/models/layout/picodet_lcnet_x1_0_fgd_layout_cdla_infer.tar",
+}
+DICT_CN = {
+ "rec_char_dict_path": "ppocr_keys_v1.txt",
+ "layout_dict_path": "layout_cdla_dict.txt",
+}
+
+
+def QImageToCvMat(incomingImage):
+ '''
+ Converts a QImage into an opencv MAT format
+ '''
+
+ incomingImage = incomingImage.convertToFormat(QImage.Format.Format_RGBA8888)
+
+ width = incomingImage.width()
+ height = incomingImage.height()
+
+ ptr = incomingImage.bits()
+ ptr.setsize(height * width * 4)
+ arr = np.frombuffer(ptr, np.uint8).reshape((height, width, 4))
+ return arr
+
+
+class APP_Image2Doc(QtWidgets.QWidget):
+ def __init__(self):
+ super(QtWidgets.QWidget, self).__init__()
+ self.pb = None # 进度条
+ self.pb_text = "已载入: {} / 已转换: {}"
+ self.imagePaths = []
+ # self.resultPath = os.path.join(here, "output")
+ self.screenShotWg = ScreenShotWidget()
+ self.screenShot = None
+ self.save_pdf = False
+
+ self.vis_font_path = os.path.join(here,
+ "doc", "fonts", "simfang.ttf")
+
+ # 初始化界面
+ self.setupUi()
+
+ # 下载模型
+ self.downloadModels(URLs_EN)
+ self.downloadModels(URLs_CN)
+
+ self.structure_sys_en = self.initPredictor('EN')
+ self.structure_sys_cn = self.initPredictor('CN')
+
+ def setupUi(self):
+ self.setObjectName("MainWindow")
+ self.setWindowTitle(__APPNAME__ + " " + __VERSION__)
+
+ layout = QtWidgets.QGridLayout()
+
+ openFileButton = QtWidgets.QPushButton("打开文件")
+ openFileButton.setIcon(QIcon(QPixmap("./icons/folder-plus.png")))
+ layout.addWidget(openFileButton, 0, 0, 1, 1)
+ openFileButton.clicked.connect(self.openFileSlot)
+
+ # screenShotButton = QtWidgets.QPushButton("截图识别")
+ # layout.addWidget(screenShotButton, 0, 1, 1, 1)
+ # screenShotButton.clicked.connect(self.screenShotSlot)
+ # screenShotButton.setEnabled(False) # temporarily disenble
+
+ startCNShotButton = QtWidgets.QPushButton("中文转换")
+ startCNShotButton.setIcon(QIcon(QPixmap("./icons/chinese.png")))
+ layout.addWidget(startCNShotButton, 0, 1, 1, 1)
+ startCNShotButton.clicked.connect(
+ functools.partial(self.startSlot, 'CN'))
+
+ startENButton = QtWidgets.QPushButton("英文转换")
+ startENButton.setIcon(QIcon(QPixmap("./icons/english.png")))
+ layout.addWidget(startENButton, 0, 2, 1, 1)
+ startENButton.clicked.connect(
+ functools.partial(self.startSlot, 'EN'))
+
+ showResultButton = QtWidgets.QPushButton("显示结果")
+ showResultButton.setIcon(QIcon(QPixmap("./icons/folder-open.png")))
+ layout.addWidget(showResultButton, 0, 3, 1, 1)
+ showResultButton.clicked.connect(self.showResultSlot)
+
+ self.pb = QtWidgets.QLabel(
+ self.pb_text.format(0, 0))
+ layout.addWidget(self.pb, 1, 0, 1, 4)
+
+ self.setLayout(layout)
+
+ def downloadModels(self, URLs):
+ # using custom model
+ tar_file_name_list = [
+ 'inference.pdiparams',
+ 'inference.pdiparams.info',
+ 'inference.pdmodel',
+ 'model.pdiparams',
+ 'model.pdiparams.info',
+ 'model.pdmodel'
+ ]
+ model_path = os.path.join(here, 'inference')
+ os.makedirs(model_path, exist_ok=True)
+
+ # download and unzip models
+ for name in URLs.keys():
+ url = URLs[name]
+ print("Try downloading file: {}".format(url))
+ tarname = url.split('/')[-1]
+ tarpath = os.path.join(model_path, tarname)
+ if os.path.exists(tarpath):
+ print("File have already exist. skip")
+ else:
+ try:
+ download_with_progressbar(url, tarpath)
+ except Exception as e:
+ print("Error occurred when downloading file, error message:")
+ print(e)
+
+ # unzip model tar
+ try:
+ with tarfile.open(tarpath, 'r') as tarObj:
+ storage_dir = os.path.join(model_path, name)
+ os.makedirs(storage_dir, exist_ok=True)
+ for member in tarObj.getmembers():
+ filename = None
+ for tar_file_name in tar_file_name_list:
+ if tar_file_name in member.name:
+ filename = tar_file_name
+ if filename is None:
+ continue
+ file = tarObj.extractfile(member)
+ with open(
+ os.path.join(storage_dir, filename),
+ 'wb') as f:
+ f.write(file.read())
+ except Exception as e:
+ print("Error occurred when unziping file, error message:")
+ print(e)
+
+ def initPredictor(self, lang='EN'):
+ # init predictor args
+ args = parse_args()
+ args.table_max_len = 488
+ args.ocr = True
+ args.recovery = True
+ args.save_pdf = self.save_pdf
+ args.table_char_dict_path = os.path.join(here,
+ "ppocr", "utils", "dict", "table_structure_dict.txt")
+ if lang == 'EN':
+ args.det_model_dir = os.path.join(here, # 此处从这里找到模型存放位置
+ "inference", "en_PP-OCRv3_det_infer")
+ args.rec_model_dir = os.path.join(here,
+ "inference", "en_PP-OCRv3_rec_infer")
+ args.table_model_dir = os.path.join(here,
+ "inference", "en_ppstructure_mobile_v2.0_SLANet_infer")
+ args.output = os.path.join(here, "output") # 结果保存路径
+ args.layout_model_dir = os.path.join(here,
+ "inference", "picodet_lcnet_x1_0_fgd_layout_infer")
+ lang_dict = DICT_EN
+ elif lang == 'CN':
+ args.det_model_dir = os.path.join(here, # 此处从这里找到模型存放位置
+ "inference", "cn_PP-OCRv3_det_infer")
+ args.rec_model_dir = os.path.join(here,
+ "inference", "cn_PP-OCRv3_rec_infer")
+ args.table_model_dir = os.path.join(here,
+ "inference", "cn_ppstructure_mobile_v2.0_SLANet_infer")
+ args.output = os.path.join(here, "output") # 结果保存路径
+ args.layout_model_dir = os.path.join(here,
+ "inference", "picodet_lcnet_x1_0_fgd_layout_cdla_infer")
+ lang_dict = DICT_CN
+ else:
+ raise ValueError("Unsupported language")
+ args.rec_char_dict_path = os.path.join(here,
+ "ppocr", "utils",
+ lang_dict['rec_char_dict_path'])
+ args.layout_dict_path = os.path.join(here,
+ "ppocr", "utils", "dict", "layout_dict",
+ lang_dict['layout_dict_path'])
+ # init predictor
+ return StructureSystem(args)
+
+ def openFileSlot(self):
+ '''
+ 可以多选图像文件
+ '''
+ selectedFiles = QtWidgets.QFileDialog.getOpenFileNames(self,
+ "多文件选择", "/", "图片文件 (*.png *.jpeg *.jpg *.bmp *.pdf)")[0]
+ if len(selectedFiles) > 0:
+ self.imagePaths = selectedFiles
+ self.screenShot = None # discard screenshot temp image
+ self.updateProgressBar(len(selectedFiles), 0)
+
+ def screenShotSlot(self):
+ '''
+ 选定图像文件和截图的转换过程只能同时进行一个
+ 截图只能同时转换一个
+ '''
+ self.screenShotWg.start()
+ if self.screenShotWg.captureImage:
+ self.screenShot = self.screenShotWg.captureImage
+ self.imagePaths.clear() # discard openfile temp list
+ self.updateProgressBar(1, 0)
+
+ def startSlot(self, lang):
+ if self.screenShot: # for screenShot
+ img_name = 'screenshot_' + time.strftime("%Y%m%d%H%M%S", time.localtime())
+ image = QImageToCvMat(self.screenShot)
+ self.predictAndSave(image, img_name, lang)
+ # update Progress Bar
+ self.updateProgressBar(1, 1)
+ QtWidgets.QMessageBox.information(self,
+ u'Information', "文档提取完成")
+ elif len(self.imagePaths) > 0 : # for image file selection
+ self.output_dir = os.path.join(
+ os.path.dirname(self.imagePaths[0]), "output") # output_dir shold be same as imagepath
+ os.makedirs(self.output_dir, exist_ok=True)
+ for i, image_file in enumerate(self.imagePaths):
+ if os.path.basename(image_file)[-3:] in ['pdf']:
+ import fitz
+ from PIL import Image
+ imgs = []
+ with fitz.open(image_file) as pdf:
+ for pg in range(0, pdf.pageCount):
+ page = pdf[pg]
+ mat = fitz.Matrix(2, 2)
+ pm = page.getPixmap(matrix=mat, alpha=False)
+
+ # if width or height > 2000 pixels, don't enlarge the image
+ if pm.width > 2000 or pm.height > 2000:
+ pm = page.getPixmap(matrix=fitz.Matrix(1, 1), alpha=False)
+
+ img = Image.frombytes("RGB", [pm.width, pm.height], pm.samples)
+ img = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
+ imgs.append(img)
+
+ else:
+ img = cv2.imread(image_file)
+ if img is None:
+ print("error in loading image:{}".format(image_file))
+ continue
+ imgs = [img]
+
+ img_name = os.path.basename(image_file).split('.')[0]
+ os.makedirs(os.path.join(self.output_dir, img_name), exist_ok=True)
+ self.predictAndSave(imgs, img_name, lang)
+
+ # update Progress Bar
+ self.updateProgressBar(len(self.imagePaths), i+1)
+ QtWidgets.QMessageBox.information(self,
+ u'Information', "文档提取完成")
+ else:
+ print('empty input')
+
+ def predictAndSave(self, imgs, img_name, lang):
+ all_res = []
+ for index, img in enumerate(imgs):
+ if lang == 'EN':
+ res, time_dict = self.structure_sys_en(img)
+ elif lang == 'CN':
+ res, time_dict = self.structure_sys_cn(img)
+
+ # save output
+ save_structure_res(res, self.output_dir, img_name)
+ draw_img = draw_structure_result(img, res, self.vis_font_path)
+ img_save_path = os.path.join(self.output_dir, img_name, 'show_{}.jpg'.format(index))
+ if res != []:
+ cv2.imwrite(img_save_path, draw_img)
+
+ # recovery
+ h, w, _ = img.shape
+ res = sorted_layout_boxes(res, w)
+ all_res += res
+
+ try:
+ convert_info_docx(img, all_res, self.output_dir, img_name, self.save_pdf)
+ except Exception as ex:
+ QtWidgets.QMessageBox.information(self,
+ u'Information', "error in layout recovery image:{}, err msg: {}".format(
+ img_name, ex))
+
+ print('result save to {}'.format(self.output_dir))
+
+ def showResultSlot(self):
+ if os.path.exists(self.output_dir):
+ if platform.system() == 'Windows':
+ os.startfile(self.output_dir)
+ else:
+ os.system('open ' + os.path.normpath(self.lastOpenDir))
+ else:
+ QtWidgets.QMessageBox.information(self,
+ u'Information', "输出文件不存在")
+
+ def updateProgressBar(self, loaded, finished):
+ self.pb.setText(
+ self.pb_text.format(loaded, finished))
+
+
+def main():
+ app = QtWidgets.QApplication(sys.argv)
+
+ window = APP_Image2Doc() # 创建对象
+ window.show() # 全屏显示窗口
+
+ QtWidgets.QApplication.processEvents()
+ sys.exit(app.exec())
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file