├── .gitignore ├── .readme-assets └── compress-pdf.png ├── LICENSE ├── README.md ├── assets ├── icon.png ├── itsfoss-logo.webp └── pdf.png ├── requirements.txt └── src └── pdf-compressor.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ### Python ### 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | env/ 32 | venv/ 33 | 34 | ### Snapcraft ### 35 | 36 | parts/ 37 | prime/ 38 | stage/ 39 | *.snap 40 | 41 | # Snapcraft global state tracking data(automatically generated) 42 | # https://forum.snapcraft.io/t/location-to-save-global-state/768 43 | /snap/.snapcraft/ 44 | 45 | # Source archive packed by `snapcraft cleanbuild` before pushing to the LXD container 46 | /*_source.tar.bz2 -------------------------------------------------------------------------------- /.readme-assets/compress-pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsfoss/compress-pdf/fe902213fba2d0353c6eadd327c7db8dd2a17a48/.readme-assets/compress-pdf.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | License 2 | ---------------------------------------------------------------------------- 3 | 4 | This file is part of It's FOSS team. 5 | 6 | PDF-Compressor is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | PDF-Compressor is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with PDF-Compressor. If not, see . 18 | 19 | It's FOSS Dev team. 20 | ----------------------------------------------------------------------------- 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Compress PDF v2.0 2 | 3 | **Compress PDF** is a simple and efficient tool designed to reduce the size of PDF files without compromising quality. This version features a sleek and modern UI built using **Qt6**, making the compression process more user-friendly. 4 | 5 | ![Compress PDF](.readme-assets/compress-pdf.png) 6 | 7 | ## About 8 | This project is built with **Python** and **PyQt6**, utilizing **Ghostscript** for PDF compression. Since I am still learning how to package Python scripts into executable binaries, I welcome any contributions, feedback, or guidance from the community to improve this tool. 9 | 10 | ## Installation 11 | 12 | ### 🔹 Required Dependencies 13 | Before installing, ensure you have the following dependencies installed: 14 | 15 | ```bash 16 | sudo apt update && sudo apt install -y python3 python3-venv python3-pip ghostscript -y 17 | ``` 18 | 19 | ### 🔹 Installing the `.deb` Package 20 | Once dependencies are installed, you can install the `.deb` package using: 21 | 22 | ```bash 23 | sudo dpkg -i pdf-compress-v1.0.deb 24 | ``` 25 | 26 | If you encounter dependency errors, fix them with: 27 | 28 | ```bash 29 | sudo apt --fix-broken install 30 | ``` 31 | 32 | ## Running the AppImage 33 | An **AppImage** version is also available for easier execution. 34 | 35 | ### 🔹 Make it Executable 36 | ```bash 37 | chmod +x pdf-compress-v1.0.AppImage 38 | ``` 39 | 40 | ### 🔹 Run the AppImage 41 | ```bash 42 | ./pdf-compress-v1.0.AppImage 43 | ``` 44 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsfoss/compress-pdf/fe902213fba2d0353c6eadd327c7db8dd2a17a48/assets/icon.png -------------------------------------------------------------------------------- /assets/itsfoss-logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsfoss/compress-pdf/fe902213fba2d0353c6eadd327c7db8dd2a17a48/assets/itsfoss-logo.webp -------------------------------------------------------------------------------- /assets/pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itsfoss/compress-pdf/fe902213fba2d0353c6eadd327c7db8dd2a17a48/assets/pdf.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyQt6 2 | humanize 3 | -------------------------------------------------------------------------------- /src/pdf-compressor.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import subprocess 4 | from PyQt6.QtWidgets import (QApplication, QWidget, QLabel, QPushButton, 5 | QFileDialog, QGridLayout, QRadioButton, QToolButton, 6 | QFrame, QSizePolicy, QLineEdit, QVBoxLayout, QHBoxLayout, QButtonGroup) 7 | from PyQt6.QtGui import QPixmap, QIcon, QPalette, QColor, QFont 8 | from PyQt6.QtCore import Qt, QSize 9 | from PyQt6.QtWidgets import QMessageBox 10 | import time 11 | from PyQt6.QtCore import QThread, pyqtSignal 12 | from PyQt6.QtCore import QMetaObject, Qt 13 | import humanize 14 | 15 | if getattr(sys, 'frozen', False): 16 | app_dir = sys._MEIPASS # PyInstaller's temp folder 17 | else: 18 | app_dir = os.path.dirname(os.path.abspath(__file__)) 19 | 20 | class WorkerThread(QThread): 21 | finished = pyqtSignal(float) # Signal to indicate when compression is done 22 | 23 | def __init__(self, command): 24 | super().__init__() 25 | self.command = command 26 | 27 | def run(self): 28 | import time # Import time inside the thread to avoid any issues 29 | start_time = time.time() 30 | 31 | try: 32 | subprocess.run(self.command, check=True) 33 | except subprocess.CalledProcessError: 34 | self.finished.emit(-1) # Emit -1 if compression fails 35 | return 36 | 37 | elapsed_time = round(time.time() - start_time, 2) 38 | self.finished.emit(elapsed_time) # Emit the time taken for compression 39 | 40 | 41 | class PdfCompressor(QWidget): 42 | def __init__(self): 43 | super().__init__() 44 | self.initUI() 45 | 46 | def compressPdf(self): 47 | print("Compressing PDF...") # Debugging print statement 48 | 49 | def initUI(self): 50 | self.setWindowTitle("Compress PDF by It's FOSS") 51 | self.resize(700, 450) 52 | self.setAcceptDrops(True) 53 | self.setStyleSheet("background-color: #F8F9FA; font-family: 'Poppins'; border-radius: 10px;") 54 | self.pdf_icon_label = QLabel() 55 | self.pdf_icon_label.setFocusPolicy(Qt.FocusPolicy.NoFocus) 56 | self.or_label = QLabel("OR") 57 | self.select_button = QPushButton("Select PDF") 58 | self.left_layout = QVBoxLayout() 59 | self.left_layout.addWidget(self.pdf_icon_label) 60 | self.left_layout.addWidget(self.or_label) 61 | self.left_layout.addWidget(self.select_button) 62 | self.left_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) 63 | 64 | # Placeholder for Logo 65 | self.logo_label = QLabel() 66 | self.logo_label.setFixedHeight(35) 67 | self.logo_label.setPixmap(QPixmap(os.path.join(app_dir, "assets/itsfoss-logo.webp"))) 68 | self.logo_label.setAlignment(Qt.AlignmentFlag.AlignLeft) 69 | 70 | # Left Panel - Drag and Drop Area 71 | self.pdf_icon_label = QLabel() 72 | self.pdf_icon_label.setPixmap(QPixmap("arc_pdf_icon.png").scaled(50, 50, Qt.AspectRatioMode.KeepAspectRatio)) 73 | self.pdf_icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter) 74 | self.pdf_icon_label.setStyleSheet("border: 4px dashed #bcbcbc; padding: 0px; border-radius: 10px;") 75 | self.pdf_icon_label.setText("Drag & Drop a PDF Here") 76 | self.pdf_icon_label.setFont(QFont("Poppins", 10, QFont.Weight.Bold)) 77 | self.pdf_icon_label.setFixedSize(260, 180) 78 | 79 | self.or_label = QLabel("OR") 80 | self.or_label.setFont(QFont("Poppins", 10, QFont.Weight.Bold)) 81 | self.or_label.setAlignment(Qt.AlignmentFlag.AlignCenter) 82 | 83 | self.select_button = QPushButton("Select PDF") 84 | self.select_button.setStyleSheet("background-color: #8fce00; color: white; padding: 8px; border-radius: 5px;") 85 | self.select_button.clicked.connect(self.selectFile) 86 | 87 | left_layout = QVBoxLayout() 88 | left_layout.addWidget(self.pdf_icon_label) 89 | left_layout.addWidget(self.or_label) 90 | left_layout.addWidget(self.select_button) 91 | left_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) 92 | 93 | # Right Panel - Compression Levels 94 | self.high_radio = QRadioButton("High") 95 | self.high_radio.setFont(QFont("Poppins", 14, QFont.Weight.Bold)) 96 | self.high_radio.setStyleSheet("color: #8fce00;") 97 | self.high_sub_label = QLabel("Best quality, larger file size") 98 | self.high_sub_label.setStyleSheet("font-size: 12px; color: #5b5b5b;") 99 | 100 | self.medium_radio = QRadioButton("Medium") 101 | self.medium_radio.setFont(QFont("Poppins", 14, QFont.Weight.Bold)) 102 | self.medium_radio.setStyleSheet("color: #8fce00;") 103 | self.medium_sub_label = QLabel("Balanced quality and file size") 104 | self.medium_sub_label.setStyleSheet("font-size: 12px; color: #5b5b5b;") 105 | 106 | self.low_radio = QRadioButton("Low") 107 | self.low_radio.setFont(QFont("Poppins", 14, QFont.Weight.Bold)) 108 | self.low_radio.setStyleSheet("color: #8fce00;") 109 | self.low_sub_label = QLabel("Smallest file size, reduced quality") 110 | self.low_sub_label.setStyleSheet("font-size: 12px; color: #5b5b5b;") 111 | 112 | 113 | # Ensuring only one button is selected at a time 114 | self.radio_group = QButtonGroup(self) 115 | self.radio_group.addButton(self.high_radio) 116 | self.radio_group.addButton(self.medium_radio) 117 | self.radio_group.addButton(self.low_radio) 118 | self.medium_radio.setChecked(True) 119 | 120 | right_layout = QVBoxLayout() 121 | for radio, sub_label in [(self.high_radio, self.high_sub_label), 122 | (self.medium_radio, self.medium_sub_label), 123 | (self.low_radio, self.low_sub_label)]: 124 | row_layout = QVBoxLayout() 125 | row_layout.addWidget(radio) 126 | row_layout.addWidget(sub_label) 127 | right_layout.addLayout(row_layout) 128 | right_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) 129 | 130 | # Separator 131 | separator = QFrame() 132 | separator.setFrameShape(QFrame.Shape.VLine) 133 | separator.setStyleSheet("border: 2px solid #CCCCCC;") 134 | 135 | # Main Layout 136 | main_layout = QHBoxLayout() 137 | main_layout.addLayout(left_layout) 138 | main_layout.addWidget(separator) 139 | main_layout.addLayout(right_layout) 140 | 141 | # Horizontal Separator 142 | horizontal_separator = QFrame() 143 | horizontal_separator.setFrameShape(QFrame.Shape.HLine) 144 | horizontal_separator.setStyleSheet("border: 2px solid #CCCCCC;") 145 | 146 | # Output Folder Section 147 | self.output_label = QLabel("Output Folder :") 148 | self.output_label.setFont(QFont("Poppins", 10, QFont.Weight.Bold)) 149 | self.output_path = QLineEdit(os.path.expanduser("~/Desktop")) 150 | self.output_path.setStyleSheet("border: 1px solid #CCCCCC; padding: 5px; border-radius: 5px;") 151 | self.output_button = QPushButton("...") 152 | self.output_button.setFixedSize(30, 30) 153 | self.output_button.setStyleSheet("border: 1px solid #0078D4; border-radius: 5px;") 154 | self.output_button.clicked.connect(self.selectOutputFolder) 155 | 156 | output_layout = QHBoxLayout() 157 | output_layout.addWidget(self.output_label) 158 | output_layout.addWidget(self.output_path) 159 | output_layout.addWidget(self.output_button) 160 | output_layout.setAlignment(Qt.AlignmentFlag.AlignRight) 161 | 162 | # Buttons Section 163 | self.cancel_button = QPushButton("Cancel") 164 | self.cancel_button.setStyleSheet("border: 1px solid #CCCCCC; border-radius: 5px; padding: 10px;") 165 | self.compress_button = QPushButton("Compress") 166 | self.compress_button.setStyleSheet("background-color: #8fce00; color: white; padding: 10px; border-radius: 5px;") 167 | self.compress_button.setCursor(Qt.CursorShape.PointingHandCursor) 168 | self.cancel_button.clicked.connect(self.close) 169 | self.compress_button.clicked.connect(self.compressPdf) 170 | self.compress_button.setEnabled(False) 171 | 172 | button_layout = QHBoxLayout() 173 | button_layout.addWidget(self.output_label) 174 | button_layout.addWidget(self.output_path) 175 | button_layout.addWidget(self.output_button) 176 | button_layout.addStretch() 177 | button_layout.addWidget(self.cancel_button) 178 | button_layout.addWidget(self.compress_button) 179 | button_layout.setAlignment(Qt.AlignmentFlag.AlignRight) 180 | 181 | # Final Layout 182 | final_layout = QVBoxLayout() 183 | final_layout.addWidget(self.logo_label) 184 | final_layout.addSpacing(20) 185 | final_layout.addLayout(main_layout) 186 | final_layout.addSpacing(10) 187 | final_layout.addWidget(horizontal_separator) 188 | final_layout.addLayout(button_layout) 189 | final_layout.addSpacing(10) 190 | 191 | self.setLayout(final_layout) 192 | 193 | def dragEnterEvent(self, event): 194 | if event.mimeData().hasUrls(): 195 | event.acceptProposedAction() 196 | 197 | def dropEvent(self, event): 198 | files = [u.toLocalFile() for u in event.mimeData().urls()] 199 | if files and files[0].lower().endswith('.pdf'): 200 | self.selected_file = files[0] 201 | self.displayPdfInfo() 202 | self.compress_button.setEnabled(True) 203 | 204 | def displayPdfInfo(self): 205 | if self.selected_file: 206 | file_name = os.path.basename(self.selected_file) 207 | 208 | # ✅ Get file size in MB and format it nicely 209 | file_size = os.path.getsize(self.selected_file) / (1024 * 1024) # Convert to MB 210 | file_size_text = f"Size: {humanize.naturalsize(file_size, binary=True)}" # e.g., "2.4 MB" 211 | 212 | # ✅ Construct the correct path for the image 213 | pdf_icon_path = os.path.join(app_dir, "assets", "pdf.png") 214 | 215 | # ✅ Load the image from the correct path 216 | pixmap = QPixmap(pdf_icon_path).scaled(50, 50, Qt.AspectRatioMode.KeepAspectRatio) 217 | 218 | # ✅ Apply the image to the label 219 | self.pdf_icon_label.setStyleSheet("border: 2px solid #0078D4; padding: 10px; border-radius: 10px;") 220 | self.pdf_icon_label.setPixmap(pixmap) 221 | 222 | # Hide original widgets 223 | self.or_label.hide() 224 | self.select_button.hide() 225 | 226 | # ✅ Clear existing layout 227 | if self.pdf_icon_label.layout(): 228 | old_layout = self.pdf_icon_label.layout() 229 | while old_layout.count(): 230 | item = old_layout.takeAt(0) 231 | widget = item.widget() 232 | if widget is not None: 233 | widget.deleteLater() 234 | del old_layout 235 | 236 | # ✅ Create a vertical layout for file info 237 | layout = QVBoxLayout() 238 | layout.setContentsMargins(5, 0, 0, 0) 239 | 240 | # ✅ PDF Icon 241 | icon_label = QLabel() 242 | icon_label.setPixmap(pixmap) 243 | icon_label.setStyleSheet("QLabel {border: none;}") 244 | layout.addWidget(icon_label, alignment=Qt.AlignmentFlag.AlignCenter) 245 | 246 | # ✅ PDF Name Label (Multi-line, Centered) 247 | file_label = QLabel(file_name) 248 | file_label.setStyleSheet("font-weight: bold;") 249 | file_label.setWordWrap(True) # ✅ Enable text wrapping 250 | file_label.setAlignment(Qt.AlignmentFlag.AlignCenter) 251 | layout.addWidget(file_label) 252 | 253 | # ✅ PDF Size Label (Dark Gray, Bold) 254 | size_label = QLabel(file_size_text) 255 | size_label.setStyleSheet("color: #5b5b5b; font-size: 12px; font-weight: bold;") 256 | size_label.setAlignment(Qt.AlignmentFlag.AlignCenter) 257 | layout.addWidget(size_label) 258 | 259 | # ✅ Apply the new layout 260 | self.pdf_icon_label.setLayout(layout) 261 | self.pdf_icon_label.setStyleSheet("border: none;") 262 | else: 263 | # Reset if no file is selected 264 | self.pdf_icon_label.setText("Drag & Drop a PDF Here") 265 | self.pdf_icon_label.setStyleSheet("border: 4px dashed #CCCCCC; padding: 30px; border-radius: 10px;") 266 | self.pdf_icon_label.setPixmap(QPixmap("arc_pdf_icon.png").scaled(50, 50, Qt.AspectRatioMode.KeepAspectRatio)) 267 | self.pdf_icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter) 268 | self.or_label.show() 269 | self.select_button.show() 270 | 271 | def selectFile(self): 272 | file_name, _ = QFileDialog.getOpenFileName(self, "Select PDF File", "", "PDF Files (*.pdf)") 273 | if file_name: 274 | self.selected_file = file_name 275 | self.displayPdfInfo() 276 | self.compress_button.setEnabled(True) 277 | 278 | def selectOutputFolder(self): 279 | folder = QFileDialog.getExistingDirectory(self, "Select Output Folder") 280 | if folder: 281 | self.output_path.setText(folder) 282 | 283 | def compressPdf(self): 284 | if not self.selected_file: 285 | return 286 | 287 | filename = os.path.splitext(os.path.basename(self.selected_file))[0] 288 | output_filename = f"compressed-{filename}.pdf" 289 | 290 | output_folder = self.output_path.text().strip() 291 | if not output_folder: 292 | output_folder = os.path.expanduser("~/Desktop") 293 | 294 | output_file = os.path.join(output_folder, output_filename) 295 | 296 | command = ["gs", "-sDEVICE=pdfwrite", "-dCompatibilityLevel=1.4", "-dPDFSETTINGS=/screen", 297 | "-dNOPAUSE", "-dBATCH", f"-sOutputFile={output_file}", self.selected_file] 298 | 299 | # ✅ Show "Compressing..." message box 300 | self.message_box = QMessageBox(self) 301 | self.message_box.setIcon(QMessageBox.Icon.Information) 302 | self.message_box.setWindowTitle("Compression in Progress") 303 | self.message_box.setText("Compressing your PDF... Hang tight! 🚀") 304 | self.message_box.setStandardButtons(QMessageBox.StandardButton.NoButton) 305 | self.message_box.show() 306 | 307 | # ✅ Start the Worker Thread for Compression 308 | self.worker = WorkerThread(command) 309 | self.worker.finished.connect(self.compressionFinished) # ✅ Fix: This function must exist! 310 | self.worker.start() # Start the background compression 311 | 312 | def compressionFinished(self, elapsed_time): # ✅ Fix: This function must exist! 313 | if hasattr(self, 'message_box') and self.message_box is not None: 314 | QMetaObject.invokeMethod(self.message_box, "accept", Qt.ConnectionType.QueuedConnection) 315 | 316 | if elapsed_time == -1: 317 | QMessageBox.critical(self, "Error", "Compression failed. Make sure Ghostscript is installed.") 318 | else: 319 | QMessageBox.information(self, "Compression Complete", f"PDF compressed successfully in {elapsed_time} seconds!") 320 | 321 | 322 | if __name__ == "__main__": 323 | app = QApplication(sys.argv) 324 | window = PdfCompressor() 325 | window.show() 326 | sys.exit(app.exec()) 327 | --------------------------------------------------------------------------------