Skip to content

[Python] PyInstaller 製作可夾帶圖片的可執行檔

Last Updated on 2021-05-13 by Clay

使用 PyInstaller 打包 Python 程式為 exe 的時候,常常碰到我們程式當中想要放圖片的時候。若是直接就使用 PyInstaller 打包,那麼我們常常會碰到執行檔案時的一個問題:『圖片顯示不出來。』

針對這個問題,其實網路上有不少辦法, 辦法從較簡單到困難,我認為有三種解決方法,並且我對第三種我認為比較好的辦法做了一點小改良。

雖然可能很多高手都知道這個辦法了,但還是容我獻醜,紀錄在這裡吧。

辦法 1辦法 2辦法 3
難易度簡單簡單較麻煩
作法 將圖片放到執行檔的目錄 使用絕對路徑讀取圖片 將圖片轉成 byte,放在程式碼中讀取
特性在自己電腦上,執行檔跟圖片都不能改資料夾只能用於自己的電腦即使分享給別人仍然可用

以下我就簡單介紹這三種辦法。


事先準備

如果是想要了解 PyInstaller 運作,也許你可以參考我之前寫過的這篇《PyInstaller —— 如何將 Python 檔案打包成 exe 執行檔》

如果想要學習 PyQt5 怎麼運作,也許可以參考我寫的《PyQt5 基本教學》

那麼以下開始。

首先,我使用 Qt Designer 直接拉出了個簡單的視窗,上面只有一個元件: QLabel。

程式碼如下:

# -*- coding: utf-8 -*-
from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(796, 554)

        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")

        self.label = QtWidgets.QLabel(self.centralwidget)
        self.label.setGeometry(QtCore.QRect(170, 70, 431, 341))
        self.label.setText("")
        self.label.setObjectName("label")

        MainWindow.setCentralWidget(self.centralwidget)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))



這裡只是介面,我另外寫了個程式繼承這個界面的 class:

# -*- coding: utf-8 -*-
import sys
import cv2
from PyQt5 import QtWidgets
from PyQt5.QtGui import *
from test import Ui_MainWindow


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)

        image = cv2.imread('test.png')
        h, w, channel = image.shape
        bytesPerLine = 3*w
        image = QImage(image, w, h, bytesPerLine, QImage.Format_RGB888).rgbSwapped()
        image = QPixmap(image)

        self.ui.label.setPixmap(image)
        self.ui.label.setScaledContents(True)


if __name__ == '__main__':
    app = QtWidgets.QApplication([])
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())



Output:

我是用 opencv 將圖片讀進來,然後再使用 QPixmap() 儲存圖片,最後顯示在我們圖形界面上的 Label 元件。

至此,準備工作告一段落。


辦法 1 —— 將圖片放到執行檔的目錄

首先我們使用 PyInstaller 打包我們的程式。

pyinstaller -F test_GUI.py --noconsole

(補充: -F 為『只打包成一個執行檔; --noconsole 為『執行檔案時取消黑視窗』』)

打包好後,我們來到打包好的 dist 目錄看看。

我在這裡只有這麼一個執行檔,雙擊執行。

這時候我們會看到這樣的『致命錯誤』—— 這是正常的,因為執行檔找不到我們要顯示的圖片。

所以辦法 1 來了 —— 我們將我們所要使用的爆炸圖片移到這個目錄底下。

這時候再執行一次。

這次就很順利地打開程式了。

不過以上的這個方法雖然簡單,卻不太推薦。


辦法 2 —— 使用絕對路徑讀取圖片

這個方法其實也是相當簡單。

image = cv2.imread('C:/Users/Clay/PycharmProjects/VideoScreenshot/test.png')



我們將讀取圖片從相對路徑更改為絕對路徑。然後我們再打包一次:

pyinstaller -F test_GUI.py --noconsole

這一次,我們在執行檔的目錄底下沒有圖片。

這張圖片的 alt 屬性值為空,它的檔案名稱為 image-65.png

雙擊執行:

這張圖片的 alt 屬性值為空,它的檔案名稱為 image-64.png

是可以顯示出圖片的,原因很簡單,因為我們使用『絕對路徑』指定了圖片的位置。但這個方法其實也不好,因為如果今天絕對路徑指定的圖片消失的話,這個程式照樣無法使用 —— 這意味著,我們仍然沒辦法分享這個程式給其他人使用。


辦法 3 —— 將圖片轉成 byte,放在程式碼中讀取

這個方法是我最推薦的方法,畢竟這是比較安全、可以分享給別人使用的作法。

首先,我們使用 base64 這個模組將圖片轉換成 byte:

# -*- coding: utf-8 -*-
import base64


def pic2str(file, functionName):
    pic = open(file, 'rb')
    content = '{} = {}\n'.format(functionName, base64.b64encode(pic.read()))
    pic.close()

    with open('pic2str.py', 'a') as f:
        f.write(content)


if __name__ == '__main__':
    pic2str('test.png', 'explode')



我所寫的 pic2str() 基本上只需要輸入『圖片路徑』以及『程式中要儲存圖片的變數名稱』兩項,就可以將圖片轉成的 byte 存入 "pic2str.py" 這個 py 檔當中的變數。

我們來看一下成果:

非常長,長到看不見盡頭。不過這就是我們的圖片。

然後我們回到原本的程式,稍微修改下:

# -*- coding: utf-8 -*-
import sys
import base64
from io import BytesIO
from PyQt5 import QtWidgets
from PyQt5.QtGui import *
from test import Ui_MainWindow
from PIL import Image, ImageQt

from pic2str import explode


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)

        # Load byte data
        byte_data = base64.b64decode(explode)
        image_data = BytesIO(byte_data)
        image = Image.open(image_data)

        # PIL to QPixmap
        qImage = ImageQt.ImageQt(image)
        image = QPixmap.fromImage(qImage)

        # QPixmap to QLabel
        self.ui.label.setPixmap(image)
        self.ui.label.setScaledContents(True)


if __name__ == '__main__':
    app = QtWidgets.QApplication([])
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())



別忘了要使用 "from pic2str import explode" 這行導入我們剛存下來的圖片 byte。

之間讀取的流程基本上為:

然後我們再次使用指令打包:

pyinstaller -F test_GUI.py --noconsole

這一次我們可以將爆炸(或你測試的圖片)通通刪掉,試跑看看:

如果能正常顯示,代表這個方法非常成功,我們可以任意分享給朋友了!

這個辦法之所以能成功,是因為 PyInstaller 並不是將 Python 檔打包為原生的 exe 檔,而是建制一個可以執行 Python 檔案的環境,所以通常這個 exe 檔都很肥。而圖片等等外部素材就是 PyInstaller 不會連結起來的物件之一,所以才會鑽這個漏洞,將圖片的 bytes 直接存入 py 檔中。

畢竟若是 py 檔都沒有成功包起來,代表這個模組真的有很大的漏洞。不過目前我使用 PyInstaller ,雖然遇到不少坑,但是基本上耐心點都是可以解決的。

如果你遇到了:

ValueError: unsupported image mode 'LA'

類似以上這樣的問題,也許你可以參考我所寫的《[Solved] ValueError: unsupported image mode ‘LA’》


References

Leave a Reply