├── README.md ├── .gitignore └── autopdf.py /README.md: -------------------------------------------------------------------------------- 1 | # PDF 壓縮工具 2 | 3 | 這是一個簡單的 PDF 壓縮工具,使用 Python 和 Tkinter 開發,能夠將 PDF 檔案轉換為較小的檔案大小。 4 | 5 | ## 使用方法 6 | 7 | 1. **下載與安裝**: 8 | - 從 GitHub 下載此專案: 9 | 先從螢幕右方的Release下載最新的Win11(V0.3)版本,選擇`PDFCompressor.exe` 10 | (Edge等瀏覽器可能會跳出不常下載等警告,您可以選擇忽略他並下載) 11 | - 透過下載原始碼來執行: 12 | 考慮多數學生可能不熟悉,將暫不放出如何操作 13 | 14 | 2. **運行應用程式**: 15 | - 使用以下命令啟動應用程式: 16 | ```bash 17 | python autopdf.py 18 | ``` 19 | 前提是你有突破剛剛的困難 自己下載requirments 20 | 21 | - 或者,如果您已經打包成可執行檔,直接雙擊 `PDFCompressor.exe`。 22 | 23 | 3. **操作步驟**: 24 | - 在應用程式中,您可以選擇要壓縮的 PDF 檔案。 25 | - 設定輸出資料夾和 DPI 值。 26 | - 點擊「開始轉換」按鈕,應用程式將開始處理檔案。 27 | - 完成後,您將看到轉換後檔案的大小。 28 | 29 | ## 注意事項 30 | 31 | - 確保您選擇的 PDF 檔案不為空,並且已選擇輸出資料夾。 32 | - 應用程式會顯示每個檔案的轉換狀態和大小。 33 | - 之前的版本因為沒有整合Poppler而導致無法運行,現在已經整合進到最新版本 34 | ## 貢獻 35 | 36 | 如果您有任何建議或想要貢獻,請隨時提出問題或提交拉取請求。 37 | 38 | ## 授權 39 | 40 | 本專案使用 MIT 授權,詳情請參閱 LICENSE 文件。 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # PyPI configuration file 171 | .pypirc 172 | -------------------------------------------------------------------------------- /autopdf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tkinter as tk 3 | from tkinter import ttk, filedialog, messagebox 4 | from pdf2image import convert_from_path 5 | from PIL import Image 6 | import threading 7 | 8 | class PDFCompressorGUI: 9 | def __init__(self, root): 10 | self.root = root 11 | self.root.title("PDF 壓縮工具") 12 | self.root.geometry("600x700") 13 | 14 | # 主框架 15 | main_frame = ttk.Frame(root, padding="10") 16 | main_frame.pack(fill=tk.BOTH, expand=True) 17 | 18 | # 檔案選擇區域 19 | file_frame = ttk.LabelFrame(main_frame, text="PDF檔案選擇", padding="5") 20 | file_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) 21 | 22 | # 檔案列表 23 | self.file_listbox = tk.Listbox(file_frame, height=10) 24 | self.file_listbox.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) 25 | 26 | # 檔案操作按鈕 27 | btn_frame = ttk.Frame(file_frame) 28 | btn_frame.pack(fill=tk.X, padx=5, pady=5) 29 | ttk.Button(btn_frame, text="添加檔案", command=self.add_files).pack(side=tk.LEFT, padx=5) 30 | ttk.Button(btn_frame, text="清除所有", command=self.clear_files).pack(side=tk.LEFT, padx=5) 31 | ttk.Button(btn_frame, text="移除選中", command=self.remove_selected).pack(side=tk.LEFT, padx=5) 32 | 33 | # 設定區域 34 | settings_frame = ttk.LabelFrame(main_frame, text="設定", padding="5") 35 | settings_frame.pack(fill=tk.X, padx=5, pady=5) 36 | 37 | # 輸出資料夾 38 | output_frame = ttk.Frame(settings_frame) 39 | output_frame.pack(fill=tk.X, padx=5, pady=5) 40 | ttk.Label(output_frame, text="輸出資料夾:").pack(side=tk.LEFT) 41 | self.output_path = tk.StringVar() 42 | ttk.Entry(output_frame, textvariable=self.output_path).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) 43 | ttk.Button(output_frame, text="瀏覽", command=self.browse_output).pack(side=tk.LEFT) 44 | 45 | # DPI 設定 46 | dpi_frame = ttk.Frame(settings_frame) 47 | dpi_frame.pack(fill=tk.X, padx=5, pady=5) 48 | ttk.Label(dpi_frame, text="DPI 設定:").pack(side=tk.LEFT) 49 | self.dpi = tk.IntVar(value=200) # 預設 DPI 50 | dpi_scale = ttk.Scale(dpi_frame, from_=72, to=300, variable=self.dpi, orient=tk.HORIZONTAL) 51 | dpi_scale.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5) 52 | self.dpi_label = ttk.Label(dpi_frame, text="200") 53 | self.dpi_label.pack(side=tk.LEFT, padx=5) 54 | dpi_scale.configure(command=self.update_dpi_label) 55 | 56 | # 目標檔案大小 57 | self.target_size = 4 * 1024 # 4MB in KB 58 | 59 | # 自動模式選項 60 | self.auto_mode = tk.BooleanVar() 61 | ttk.Checkbutton(settings_frame, text="自動調整DPI以達到目標大小", variable=self.auto_mode).pack(side=tk.LEFT, padx=5) 62 | 63 | # 進度顯示區域 64 | progress_frame = ttk.LabelFrame(main_frame, text="處理進度", padding="5") 65 | progress_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) 66 | 67 | # 進度條 68 | self.progress_var = tk.DoubleVar() 69 | self.progress_bar = ttk.Progressbar(progress_frame, variable=self.progress_var, maximum=100) 70 | self.progress_bar.pack(fill=tk.X, padx=5, pady=5) 71 | 72 | # 狀態顯示 73 | self.status_text = tk.Text(progress_frame, height=10, wrap=tk.WORD) 74 | self.status_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) 75 | 76 | # 開始按鈕 77 | self.start_button = ttk.Button(main_frame, text="開始轉換", command=self.start_conversion) 78 | self.start_button.pack(pady=10) 79 | 80 | # 儲存檔案路徑 81 | self.pdf_files = [] 82 | 83 | def update_dpi_label(self, value): 84 | self.dpi_label.configure(text=str(int(float(value)))) 85 | 86 | def add_files(self): 87 | files = filedialog.askopenfilenames( 88 | title="選擇PDF檔案", 89 | filetypes=[("PDF files", "*.pdf"), ("All files", "*.*")] 90 | ) 91 | for file in files: 92 | if file not in self.pdf_files: 93 | self.pdf_files.append(file) 94 | self.file_listbox.insert(tk.END, os.path.basename(file)) 95 | 96 | def clear_files(self): 97 | self.file_listbox.delete(0, tk.END) 98 | self.pdf_files.clear() 99 | 100 | def remove_selected(self): 101 | selection = self.file_listbox.curselection() 102 | for index in reversed(selection): 103 | self.file_listbox.delete(index) 104 | self.pdf_files.pop(index) 105 | 106 | def browse_output(self): 107 | folder = filedialog.askdirectory() 108 | if folder: 109 | self.output_path.set(folder) 110 | 111 | def update_status(self, message): 112 | self.status_text.insert(tk.END, message + "\n") 113 | self.status_text.see(tk.END) 114 | 115 | def convert_pdf_to_jpg(self, input_path, dpi): 116 | images = convert_from_path(input_path, dpi=dpi) 117 | jpg_files = [] 118 | 119 | for i, image in enumerate(images): 120 | jpg_path = f"temp_page_{i}.jpg" 121 | image = image.convert("RGB") 122 | image.save(jpg_path, 'JPEG') # 不調整品質 123 | jpg_files.append(jpg_path) 124 | 125 | return jpg_files 126 | 127 | def create_pdf_from_jpg(self, jpg_files, output_path): 128 | images = [Image.open(jpg_file) for jpg_file in jpg_files] 129 | images[0].save(output_path, save_all=True, append_images=images[1:]) 130 | 131 | for jpg_file in jpg_files: 132 | os.remove(jpg_file) 133 | 134 | def process_files(self): 135 | output_folder = self.output_path.get() 136 | 137 | if not os.path.exists(output_folder): 138 | os.makedirs(output_folder) 139 | 140 | total_files = len(self.pdf_files) 141 | if total_files == 0: 142 | self.update_status("請選擇要轉換的PDF檔案!") 143 | return 144 | 145 | self.update_status(f"開始處理 {total_files} 個檔案") 146 | 147 | for input_path in self.pdf_files: 148 | filename = os.path.basename(input_path) 149 | output_path = os.path.join(output_folder, f"converted_{filename.replace('.pdf', '.pdf')}") 150 | 151 | if self.auto_mode.get(): 152 | current_dpi = self.dpi.get() 153 | while True: 154 | jpg_files = self.convert_pdf_to_jpg(input_path, current_dpi) 155 | self.create_pdf_from_jpg(jpg_files, output_path) 156 | 157 | # 檢查輸出檔案大小 158 | output_size = os.path.getsize(output_path) / 1024 # 轉換為 KB 159 | if output_size <= self.target_size: 160 | self.update_status(f"檔案 {filename} 轉換完成,大小: {output_size:.2f} KB") 161 | break 162 | else: 163 | # 計算與目標大小的差距 164 | size_difference = output_size - self.target_size 165 | # 根據差距調整 DPI 166 | adjustment_factor = max(1, int(size_difference / 100)) # 每 100KB 調整一次 167 | current_dpi -= adjustment_factor 168 | if current_dpi < 72: # 最小 DPI 限制 169 | self.update_status(f"無法達到目標大小,最小DPI為72,檔案大小: {output_size:.2f} KB") 170 | break 171 | else: 172 | 173 | self.update_status(f"\n處理檔案: {filename}") 174 | jpg_files = self.convert_pdf_to_jpg(input_path, self.dpi.get()) 175 | self.create_pdf_from_jpg(jpg_files, output_path) 176 | output_size = os.path.getsize(output_path) / 1024 # 轉換為 KB 177 | self.update_status(f"檔案 {filename} 轉換完成,大小: {output_size:.2f} KB") 178 | 179 | self.update_status("\n所有檔案處理完成!") 180 | self.start_button.config(state=tk.NORMAL) 181 | messagebox.showinfo("完成", "PDF轉換完成!") 182 | 183 | def start_conversion(self): 184 | if not self.pdf_files: 185 | messagebox.showerror("錯誤", "請選擇要轉換的PDF檔案!") 186 | return 187 | 188 | if not self.output_path.get(): 189 | messagebox.showerror("錯誤", "請選擇輸出資料夾!") 190 | return 191 | 192 | self.start_button.config(state=tk.DISABLED) 193 | self.status_text.delete(1.0, tk.END) 194 | self.progress_var.set(0) 195 | 196 | threading.Thread(target=self.process_files, daemon=True).start() 197 | 198 | def main(): 199 | root = tk.Tk() 200 | app = PDFCompressorGUI(root) 201 | root.mainloop() 202 | 203 | if __name__ == "__main__": 204 | main() 205 | --------------------------------------------------------------------------------