Streamlit做手繪簽名

前言

工程執行過程免不了會有很多品管文件需要製作,紙本文件的加值應用相當困難,通常都會放置於一堆又一堆的資料夾中,有需要再去相互參照,大多流於形式,倘若工程文件能夠數位化,對於整個工程資料的保存、查詢會有很多好處,但經過幾次查核委員的建議,關於簽名這件事情他們有意見,既然是需要在現場進行檢查的文件,那當然會是手寫的才最香,殊不知很多委員還是認為電腦直接出的文件不能用,至少簽名的部分要手寫才是真的。

基於這些理由,我希望能夠做到用手寫的觸感去簽署文件,透過Streamlit的前端及streamlit-drawable-canvas作為繪圖器,opencv作為背景去背處理,及PyMuPDF作為轉檔工具,來達成委員的要求...

其實我一直不能理解,中華電信都可以用手寫的觸控板來跟客戶簽約了,為什麼公共工程一定要用紙+筆,現在其實平板電腦都很方便,把要抽查的PDF都先放好在雲端,帶去工地現場選取要抽查的對應PDF後寫紀錄表,寫完後可以即時上傳到雲端,資訊流動才會迅速,也不用這樣浪費一堆紙來記錄這些內容,未來要翻閱從雲端去撈取出來即可。

🎬影片操作

安裝包

1
pip install streamlit streamlit-drawable-canvas numpy opencv-python-headless Pillow PyMuPDF

圖片簽名預覽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import streamlit as st
from streamlit_drawable_canvas import st_canvas
import numpy as np
import cv2
from PIL import Image

# 設置 Streamlit 界面
st.title("圖片簽名工具")

# 上傳圖片
uploaded_file = st.file_uploader("上傳圖片", type=["jpg", "jpeg", "png"])

# 繪製簽名
st.write("繪製簽名")
canvas_result = st_canvas(
fill_color="rgba(0, 0, 0, 0)", # 背景填充色
stroke_width=3,
stroke_color="black",
background_color="rgba(255, 255, 255, 0)",
height=150,
width=400,
drawing_mode="freedraw",
key="canvas",
)

if uploaded_file is not None and canvas_result.image_data is not None:
# 讀取上傳的圖片
file_bytes = np.asarray(bytearray(uploaded_file.read()), dtype=np.uint8)
uploaded_image = cv2.imdecode(file_bytes, cv2.IMREAD_UNCHANGED)

# 獲取簽名並處理透明背景
signature_data = canvas_result.image_data
signature = cv2.cvtColor(signature_data, cv2.COLOR_RGBA2BGRA)

# 將簽名大小調整為合適大小
sig_h, sig_w, _ = signature.shape
img_h, img_w, _ = uploaded_image.shape
x_offset = img_w - sig_w - 10
y_offset = img_h - sig_h - 10

# 將簽名放置在圖片上
for c in range(0, 3):
uploaded_image[y_offset:y_offset+sig_h, x_offset:x_offset+sig_w, c] = \
signature[:, :, c] * (signature[:, :, 3] / 255.0) + \
uploaded_image[y_offset:y_offset+sig_h, x_offset:x_offset+sig_w, c] * (1.0 - signature[:, :, 3] / 255.0)

# 顯示合併後的圖片
st.image(uploaded_image, caption='簽名後的圖片', use_column_width=True)

直接簽在圖片上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import streamlit as st
from streamlit_drawable_canvas import st_canvas
import numpy as np
import cv2
from PIL import Image
import io
import fitz # PyMuPDF

# 設置 Streamlit 界面
st.title("PDF 簽名工具")

# 上傳 PDF
uploaded_file = st.file_uploader("上傳 PDF", type=["pdf"])

# 如果上傳了文件
if uploaded_file is not None:
# 讀取上傳的 PDF
pdf_bytes = uploaded_file.read()
document = fitz.open("pdf", pdf_bytes)
page = document.load_page(0)
pix = page.get_pixmap()
background_image = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)

# st.write("簽名調整")

# 繪製簽名
# st.write("繪製簽名")
canvas_result = st_canvas(
fill_color="rgba(0, 0, 0, 0)", # 背景填充色
stroke_width=3,
stroke_color="black",
background_image=background_image,
height=pix.height,
width=pix.width,
drawing_mode="freedraw",
key="canvas",
)

# 滑桿調整簽名位置
# st.write("調整簽名位置")
x_offset =0# st.slider("水平位置", 0, pix.width, 0)
y_offset =0# st.slider("垂直位置", 0, pix.height, 0)

if canvas_result.image_data is not None:
# 獲取簽名並處理透明背景
signature_data = canvas_result.image_data
signature = cv2.cvtColor(signature_data, cv2.COLOR_RGBA2BGRA)

signed_images = []

for page_num in range(len(document)):
page = document.load_page(page_num)
pix = page.get_pixmap()
image = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)

# 將 PIL 圖像轉換為 OpenCV 圖像
open_cv_image = np.array(image)
open_cv_image = cv2.cvtColor(open_cv_image, cv2.COLOR_RGB2BGRA)

# 簽名位置
sig_h, sig_w, _ = signature.shape
img_h, img_w, _ = open_cv_image.shape

# 確保簽名不超出邊界
if y_offset + sig_h > img_h:
sig_h = img_h - y_offset
if x_offset + sig_w > img_w:
sig_w = img_w - x_offset

# 簽名形狀匹配
resized_signature = cv2.resize(signature, (sig_w, sig_h))

# 將簽名放置在圖片上
for c in range(0, 3):
open_cv_image[y_offset:y_offset+sig_h, x_offset:x_offset+sig_w, c] = \
resized_signature[:, :, c] * (resized_signature[:, :, 3] / 255.0) + \
open_cv_image[y_offset:y_offset+sig_h, x_offset:x_offset+sig_w, c] * (1.0 - resized_signature[:, :, 3] / 255.0)

# 將 OpenCV 圖像轉換回 PIL 圖像
signed_image = Image.fromarray(cv2.cvtColor(open_cv_image, cv2.COLOR_BGRA2RGBA))
signed_images.append(signed_image)

# 預覽簽名後的第一頁
# st.image(signed_images[0], caption='簽名後的 PDF 頁面預覽', use_column_width=True)

# 將簽名後的圖像保存回 PDF
pdf_output = io.BytesIO()
signed_images[0].save(pdf_output, "PDF", resolution=100.0, save_all=True, append_images=signed_images[1:])
pdf_output.seek(0)

# 提供下載鏈接
st.download_button(
label="下載簽名後的 PDF",
data=pdf_output,
file_name="signed_document.pdf",
mime="application/pdf"
)

比較完整做法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
import streamlit as st
from streamlit_drawable_canvas import st_canvas
import numpy as np
import cv2
from PIL import Image
import io
import fitz # PyMuPDF
import os
import pandas as pd

st.set_page_config(
layout="wide",
)

dict_pdf = {
"測量工程抽查表": "6-27.pdf",
"土方工程抽查表": "6-29.pdf",
"模板工程抽查表": "6-30.pdf",
"鋼筋工程抽查表": "6-31.pdf",
"其他": ""
}

st.sidebar.title(" PDF 簽名工具")

# 頁面導航
page = st.sidebar.selectbox("選擇頁面", ["手繪簽名", "查看表單"])

# 根據選擇的頁面顯示內容
if page == "手繪簽名":

# 設置 Streamlit 界面
# st.title("PDF 簽名工具")

# 指定保存的文件夾路徑
save_folder = "saved_pdfs"
os.makedirs(save_folder, exist_ok=True)
form_selection = st.sidebar.selectbox("選擇抽查表", list(dict_pdf.keys()))

file_name = "./pdfs/"+dict_pdf[form_selection]

# 如果選擇了“其他”,讓用戶上傳 PDF 文件
if form_selection == "其他":
uploaded_file = st.file_uploader("上傳 PDF", type=["pdf"])
else:
# 否則,讀取預定義的 PDF 文件
with open(file_name, "rb") as f:
uploaded_file = f.read()

if uploaded_file:
if form_selection == "其他":
pdf_bytes = uploaded_file.read()
else:
pdf_bytes = uploaded_file

# 讀取 PDF
document = fitz.open("pdf", pdf_bytes)
page = document.load_page(0)
pix = page.get_pixmap(matrix=fitz.Matrix(2.0, 2.0)) # 使用 matrix 調整分辨率
original_width, original_height = pix.width, pix.height
background_image = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)

# 繪製簽名
canvas_result = st_canvas(
fill_color="rgba(0, 0, 0, 0)", # 背景填充色
stroke_width=3,
stroke_color="black",
background_image=background_image,
height=pix.height,
width=pix.width,
drawing_mode="freedraw",
key="canvas",
)

if canvas_result.image_data is not None:
# 獲取簽名並處理透明背景
signature_data = canvas_result.image_data
signature = cv2.cvtColor(signature_data, cv2.COLOR_RGBA2BGRA)

signed_images = []

# 設置簽名位置 (預設值,可以根據需要調整)
y_offset, x_offset = 0,0

for page_num in range(len(document)):
page = document.load_page(page_num)
pix = page.get_pixmap(matrix=fitz.Matrix(2.0, 2.0)) # 使用相同的 matrix 調整分辨率
image = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)

# 將 PIL 圖像轉換為 OpenCV 圖像
open_cv_image = np.array(image)
open_cv_image = cv2.cvtColor(open_cv_image, cv2.COLOR_RGB2BGRA)

# 簽名位置
sig_h, sig_w, _ = signature.shape
img_h, img_w, _ = open_cv_image.shape

# 調整簽名位置以匹配新的解析度
adjusted_y_offset = int((y_offset / original_height) * img_h)
adjusted_x_offset = int((x_offset / original_width) * img_w)

# 確保簽名不超出邊界
if adjusted_y_offset + sig_h > img_h:
sig_h = img_h - adjusted_y_offset
if adjusted_x_offset + sig_w > img_w:
sig_w = img_w - adjusted_x_offset

# 簽名形狀匹配
resized_signature = cv2.resize(signature, (sig_w, sig_h))

# 將簽名放置在圖片上
for c in range(0, 3):
open_cv_image[adjusted_y_offset:adjusted_y_offset+sig_h, adjusted_x_offset:adjusted_x_offset+sig_w, c] = \
resized_signature[:, :, c] * (resized_signature[:, :, 3] / 255.0) + \
open_cv_image[adjusted_y_offset:adjusted_y_offset+sig_h, adjusted_x_offset:adjusted_x_offset+sig_w, c] * (1.0 - resized_signature[:, :, 3] / 255.0)

# 將 OpenCV 圖像轉換回 PIL 圗像
signed_image = Image.fromarray(cv2.cvtColor(open_cv_image, cv2.COLOR_BGRA2RGBA))
signed_images.append(signed_image)

# 將簽名後的圖像保存回 PDF
pdf_output = io.BytesIO()
signed_images[0].save(pdf_output, "PDF", resolution=100.0, save_all=True, append_images=signed_images[1:])
pdf_output.seek(0)

# 增加“儲存”按鈕
if st.button("儲存", type='primary'):
# 生成保存路徑和文件名
base_file_name = form_selection if form_selection != "其他" else "custom"
existing_files = [f for f in os.listdir(save_folder) if f.startswith(base_file_name)]
file_count = len(existing_files) + 1
output_file_name = f"{base_file_name}_{file_count}.pdf"
save_path = os.path.join(save_folder, output_file_name)

# 將簽名後的 PDF 文件保存到指定資料夾
with open(save_path, "wb") as f:
f.write(pdf_output.getbuffer())
st.success(f"已將簽名後的 PDF 保存到: {save_path}")

elif page == "查看表單":

# 指定保存的文件夾路徑
save_folder = "saved_pdfs"
os.makedirs(save_folder, exist_ok=True)

# 獲取所有已保存的表單
saved_forms = os.listdir(save_folder)

# 根據文件名分類表單
classified_forms = {}
for form in saved_forms:
form_type = form.rsplit('_', 1)[0]
if form_type not in classified_forms:
classified_forms[form_type] = []
classified_forms[form_type].append(form)

# 將分類表單轉換為 DataFrame
df = pd.DataFrame(
[(key, form) for key, forms in classified_forms.items() for form in forms],
columns=["表單類型", "文件名"]
)

# 顯示 DataFrame 並允許用戶選擇要顯示的表單
selected_forms = st.multiselect("選擇要顯示的表單", df["文件名"])

# 顯示選擇的表單
if selected_forms:

for form in selected_forms:
form_path = os.path.join(save_folder, form)

st.markdown(f"#### {form}")

# 使用 PyMuPDF 加載 PDF 文件
pdf_doc = fitz.open(form_path)

# 顯示每一頁的圖像內容
for page_num in range(len(pdf_doc)):
page = pdf_doc.load_page(page_num)

# 將 PDF 頁面轉換為圖像
pix = page.get_pixmap(matrix=fitz.Matrix(2.0, 2.0)) # 使用相同的 matrix 調整分辨率
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)

st.image(img, caption=f"第 {page_num + 1} 頁")

else:
st.write("目前沒有已保存的表單。")

以前做的專案

Excel VBA@簽名檔小工具

去年曾經有把簽名檔做成很多不同照片來調整大小及位置進行貼上,不過經廣大網友的檢視後,發現這樣委員還是會不喜歡,還是先做個紀錄給大家參考參考...