├── .gitignore ├── .pylintrc ├── .vscode └── settings.json ├── README.md ├── comepress.py ├── comepress.spec ├── dist └── res │ ├── checked.svg │ ├── folder.png │ ├── inbox_tray_3d.ico │ ├── logo.png │ └── unchecked.svg ├── requirements.txt └── res ├── checked.svg ├── comepress.ui ├── folder.png ├── inbox_tray_3d.ico ├── logo.png └── unchecked.svg /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/*.zip 3 | dist/comepress 4 | env/ -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | extension-pkg-whitelist=PyQt5 -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.flake8Enabled": false, 3 | "python.linting.pydocstyleEnabled": false, 4 | "python.linting.pylintEnabled": true, 5 | "python.linting.enabled": true 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # 📥 Comepress 4 | #### Super trivial app to optimize your web project by converting all PNG, JPG and JPEG images to Next-Gen WebP format 5 | Just drag and drop your files or folders and that's it! 6 | ![image](https://user-images.githubusercontent.com/25067102/185523884-85b1731c-c4c7-4b90-bf39-c1eab6107a54.png) 7 | ![comepress](https://user-images.githubusercontent.com/25067102/185751405-2dedb81b-7e40-4332-947e-decad890a87b.gif) 8 | 9 |
10 | 11 | # Reduce your image bundle size by 40-80% 12 | #### That means faster site loading for users and smaller project size! 13 | 14 | ## Here's a small comparison: 15 | | Without Comepress | With Comepress | 16 | |-------------------|----------------| 17 | |![image](https://user-images.githubusercontent.com/25067102/185750498-91c1bf0a-d094-4caf-9eda-5054a5c990dc.png)|![image](https://user-images.githubusercontent.com/25067102/185750604-d87f63e4-37da-4f36-91b8-717aaffd48b9.png)| 18 | # With Comepress I easily got over 43% space gain! 19 | The images generated by Comepress are currently lossy but the quality is indistinguishable, so you can use this in production without any worries. 20 | 21 | # Installation 👨‍💻 22 | 23 | ## Linux 🐧 24 | 25 | 1. Download the Linux version from here: https://github.com/NayamAmarshe/comepress/releases/latest 26 | 2. Extract the 7z file. 27 | 3. Double click and run the `comepress` executable file. 28 | 29 | ## MacOS 🍎 30 | 31 | Coming Soon 32 | 33 | ## Windows 🔳 34 | 35 | Coming Soon 36 | 37 | # Build Instructions 🛠 38 | 39 | Please help me generate builds for MacOS and Windows. 40 | 41 | 1. Install dependencies: 42 | ```bash 43 | pip3 install pyqt5 pyinstaller 44 | ``` 45 | 2. To run comepress as is, run: 46 | ```bash 47 | python3 comepress.py 48 | ``` 49 | 3. To build comepress: 50 | ```bash 51 | pyinstaller comepress.py --add-data "./res/*:res" --onefile --icon='res/inbox_tray_3d.ico' --windowed 52 | ``` 53 | 54 | An executable file will be created in `dist` folder. 55 | 56 | # Credits ⛑ 57 | - [@TGS963](https://github.com/TGS963) for helping me with the project. 58 | - Microsoft for their open source Fluent 3D Emoji Icons. 59 | - Python and Qt5 60 | -------------------------------------------------------------------------------- /comepress.py: -------------------------------------------------------------------------------- 1 | import fnmatch 2 | import os 3 | import shutil 4 | import sys 5 | from os.path import isdir, join 6 | 7 | from PIL import Image 8 | from PyQt5 import QtCore, QtGui, QtWidgets, uic 9 | from PyQt5.QtGui import QCursor 10 | from PyQt5.QtWidgets import * 11 | 12 | 13 | bundle_dir = getattr(sys, "_MEIPASS", os.path.abspath(os.path.dirname(__file__))) 14 | 15 | 16 | def restart_program(): 17 | python = sys.executable 18 | os.execl(python, python, *sys.argv) 19 | 20 | 21 | def include_patterns(*patterns): 22 | def _ignore_patterns(path, all_names): 23 | # Determine names which match one or more patterns (that shouldn't be 24 | # ignored). 25 | keep = ( 26 | name for pattern in patterns for name in fnmatch.filter(all_names, pattern) 27 | ) 28 | # Ignore file names which *didn't* match any of the patterns given that 29 | # aren't directory names. 30 | dir_names = (name for name in all_names if isdir(join(path, name))) 31 | return set(all_names) - set(keep) - set(dir_names) 32 | 33 | return _ignore_patterns 34 | 35 | 36 | def ignore_list(path, files): 37 | filesToIgnore = [] 38 | for fileName in files: 39 | fullFileName = os.path.join(os.path.normpath(path), fileName) 40 | if ( 41 | not os.path.isdir(fullFileName) 42 | and not fileName.endswith("jpg") 43 | and not fileName.endswith("jpeg") 44 | and not fileName.endswith("png") 45 | and not fileName.endswith("mp4") 46 | ): 47 | filesToIgnore.append(fileName) 48 | return filesToIgnore 49 | 50 | 51 | class MyGUI(QMainWindow): 52 | def __init__(self): 53 | super(MyGUI, self).__init__() 54 | uic.loadUi(os.path.abspath(os.path.join(bundle_dir, "res/comepress.ui")), self) 55 | 56 | # Remove Titlebar and background 57 | self.setWindowFlags(QtCore.Qt.FramelessWindowHint) 58 | self.setAttribute(QtCore.Qt.WA_TranslucentBackground) 59 | self.setStyleSheet( 60 | """QToolTip { 61 | border: none; 62 | color: white; 63 | }""" 64 | ) 65 | self.setWindowIcon(QtGui.QIcon("res/inbox_tray_3d.ico")) 66 | # BUTTONS 67 | self.pushButton.clicked.connect(self.browse) 68 | 69 | self.checkBox.clicked.connect(self.checked) 70 | self.checkBox.setCursor(QCursor(QtCore.Qt.PointingHandCursor)) 71 | 72 | self.closeButton.clicked.connect(self.close) 73 | self.closeButton.setToolTip("Close") 74 | self.closeButton.setCursor(QCursor(QtCore.Qt.PointingHandCursor)) 75 | 76 | self.minimizeButton.clicked.connect(self.showMinimized) 77 | self.minimizeButton.setToolTip("Minimize") 78 | self.minimizeButton.setCursor(QCursor(QtCore.Qt.PointingHandCursor)) 79 | 80 | self.comboBox.activated.connect(self.comboBoxSelected) 81 | 82 | self.horizontalSlider.valueChanged.connect(self.horizontalSliderChanged) 83 | self.setAcceptDrops(True) 84 | 85 | # DEFAULT VARIABLES 86 | self.backup = True 87 | self.dragPos = QtCore.QPoint() 88 | self.quality = 100 89 | self.percentageLabel.setText(f"{self.quality}") 90 | self.format = "JPG" 91 | 92 | self.show() 93 | 94 | def mousePressEvent(self, event): 95 | self.dragPos = event.globalPos() 96 | 97 | def mouseMoveEvent(self, event): 98 | if event.buttons() == QtCore.Qt.LeftButton: 99 | self.move(self.pos() + event.globalPos() - self.dragPos) 100 | self.dragPos = event.globalPos() 101 | event.accept() 102 | 103 | def checked(self): 104 | self.backup = bool(self.checkBox.isChecked()) 105 | 106 | def browse(self): 107 | folder_path = QtWidgets.QFileDialog.getExistingDirectory( 108 | self, "Select a Folder" 109 | ) 110 | if folder_path == "": 111 | return 112 | self.comepress_folder(folder_path) 113 | alert_dialog = QMessageBox.information( 114 | self, "All good!", "Successfully comepressed all files/folders" 115 | ) 116 | 117 | def comboBoxSelected(self): 118 | self.format = self.comboBox.currentText() 119 | 120 | def horizontalSliderChanged(self): 121 | self.percentageLabel.setText(f"{self.horizontalSlider.value()}") 122 | self.quality = self.horizontalSlider.value() 123 | 124 | def dragEnterEvent(self, event): 125 | if event.mimeData().hasUrls(): 126 | event.accept() 127 | else: 128 | event.ignore() 129 | 130 | def dropEvent(self, event): 131 | allowed_types = ["image/jpeg", "image/jpg", "image/png", "inode/directory"] 132 | dropped_files_folders = [] 133 | db = QtCore.QMimeDatabase() 134 | 135 | for url in event.mimeData().urls(): 136 | mimetype = db.mimeTypeForUrl(url) 137 | if mimetype.name() in allowed_types: 138 | dropped_files_folders.append((url.toLocalFile(), mimetype.name())) 139 | 140 | for file_folder_path in dropped_files_folders: 141 | if file_folder_path[1] == "inode/directory": 142 | self.comepress_folder(file_folder_path[0]) 143 | 144 | else: 145 | self.comepress_file(file_folder_path[0]) 146 | alert_dialog = QMessageBox.information( 147 | self, "All good!", "Successfully comepressed all files/folders" 148 | ) 149 | 150 | def comepress_folder(self, folder_path): 151 | # Get parent folder path 152 | # Get folder name 153 | folder_name = os.path.basename(folder_path) 154 | # Destination backup 155 | if os.path.isdir(f"{folder_path}_COMEPRESS"): 156 | shutil.rmtree(f"{folder_path}_COMEPRESS") 157 | backup_destination = os.path.abspath(f"{folder_path}_COMEPRESS") 158 | # Backup folder 159 | print(f"backup: {self.backup}") 160 | if self.backup: 161 | shutil.copytree( 162 | folder_path, 163 | backup_destination, 164 | ignore=include_patterns("*.png", "*.jpg", "*.jpeg"), 165 | ) 166 | 167 | # Loop through all the files in the folder 168 | for root, dirs, files in os.walk( 169 | folder_path, 170 | ): 171 | for file in files: 172 | # If file is an image 173 | if file.endswith((".jpg", ".jpeg", ".png", ".webp")): 174 | # Convert to WebP 175 | try: 176 | img = Image.open(f"{root}/{file}") 177 | img.save( 178 | ( 179 | f"{root}/{file.rsplit('.', 1)[0]}.{self.format}", 180 | self.format, 181 | ), 182 | quality=self.quality, 183 | optimize=(self.quality < 100), 184 | ) 185 | # Remove original file 186 | os.remove(f"{root}/{file}") 187 | except Exception: 188 | alert_dialog = QMessageBox.information( 189 | self, 190 | "Error!", 191 | f"Please check if {root}/{file} is not corrupt", 192 | ) 193 | 194 | return 195 | 196 | def comepress_file(self, file_path): 197 | # GET DETAILS 198 | parent_folder = os.path.abspath(os.path.join(file_path, os.pardir)) 199 | file_name = os.path.basename(file_path) 200 | destination_path = f"{parent_folder}/ORIGINAL_{file_name}" 201 | # BACKUP 202 | if self.backup: 203 | shutil.copyfile(file_path, destination_path) 204 | # CONVERT 205 | try: 206 | img = Image.open(f"{parent_folder}/{file_name}") 207 | img.save( 208 | ((f"{parent_folder}/" + file_name.rsplit(".", 1)[0]) + ".webp"), "webp" 209 | ) 210 | os.remove(f"{parent_folder}/{file_name}") 211 | except Exception: 212 | alert_dialog = QMessageBox.information( 213 | self, 214 | "Error!", 215 | f"Please check if {parent_folder}/{file_name} is not corrupt", 216 | ) 217 | return 218 | 219 | 220 | def main(): 221 | app = QApplication([]) 222 | app.setWindowIcon(QtGui.QIcon("res/inbox_tray_3d.ico")) 223 | window = MyGUI() 224 | app.exec() 225 | 226 | 227 | if __name__ == "__main__": 228 | main() 229 | -------------------------------------------------------------------------------- /comepress.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | 4 | block_cipher = None 5 | 6 | 7 | a = Analysis( 8 | ['comepress.py'], 9 | pathex=[], 10 | binaries=[], 11 | datas=[('./res/*', 'res')], 12 | hiddenimports=[], 13 | hookspath=[], 14 | hooksconfig={}, 15 | runtime_hooks=[], 16 | excludes=[], 17 | win_no_prefer_redirects=False, 18 | win_private_assemblies=False, 19 | cipher=block_cipher, 20 | noarchive=False, 21 | ) 22 | pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) 23 | 24 | exe = EXE( 25 | pyz, 26 | a.scripts, 27 | a.binaries, 28 | a.zipfiles, 29 | a.datas, 30 | [], 31 | name='comepress', 32 | debug=False, 33 | bootloader_ignore_signals=False, 34 | strip=False, 35 | upx=True, 36 | upx_exclude=[], 37 | runtime_tmpdir=None, 38 | console=False, 39 | disable_windowed_traceback=False, 40 | argv_emulation=False, 41 | target_arch=None, 42 | codesign_identity=None, 43 | entitlements_file=None, 44 | icon='res/inbox_tray_3d.ico', 45 | ) 46 | -------------------------------------------------------------------------------- /dist/res/checked.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/res/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NayamAmarshe/comepress/f688fca431b9d1bd89adb3157757496dae51e1ba/dist/res/folder.png -------------------------------------------------------------------------------- /dist/res/inbox_tray_3d.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NayamAmarshe/comepress/f688fca431b9d1bd89adb3157757496dae51e1ba/dist/res/inbox_tray_3d.ico -------------------------------------------------------------------------------- /dist/res/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NayamAmarshe/comepress/f688fca431b9d1bd89adb3157757496dae51e1ba/dist/res/logo.png -------------------------------------------------------------------------------- /dist/res/unchecked.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow==9.5.0 2 | Pillow==9.5.0 3 | PyQt5==5.15.9 4 | PyQt5_sip==12.12.1 5 | -------------------------------------------------------------------------------- /res/checked.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/comepress.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 500 10 | 684 11 | 12 | 13 | 14 | MainWindow 15 | 16 | 17 | 1.000000000000000 18 | 19 | 20 | 21 | 22 | 23 | 10 24 | 10 25 | 480 26 | 661 27 | 28 | 29 | 30 | QFrame { 31 | background: #28262b; 32 | border-radius: 25px; 33 | border: 2px solid #65606C; 34 | } 35 | 36 | QHBoxLayout { 37 | border: none; 38 | } 39 | 40 | 41 | QFrame::StyledPanel 42 | 43 | 44 | QFrame::Raised 45 | 46 | 47 | 48 | 49 | 30 50 | 140 51 | 420 52 | 250 53 | 54 | 55 | 56 | false 57 | 58 | 59 | background: #2a2c2e; 60 | border-radius: 25px; 61 | border: 2px solid #323134; 62 | 63 | 64 | QFrame::StyledPanel 65 | 66 | 67 | QFrame::Raised 68 | 69 | 70 | 71 | 72 | 150 73 | 40 74 | 126 75 | 121 76 | 77 | 78 | 79 | border-image: url(res/folder.png) 0 0 0 0 stretch stretch; 80 | 81 | 82 | false 83 | 84 | 85 | 86 | 87 | 88 | 70 89 | 170 90 | 271 91 | 41 92 | 93 | 94 | 95 | 96 | Montserrat 97 | 50 98 | false 99 | 100 | 101 | 102 | border: none; 103 | border-radius: none; 104 | background: none; 105 | color: #ecedec; 106 | 107 | 108 | Drag and Drop your Files or Folders here or Click to Select Files... 109 | 110 | 111 | Qt::AlignCenter 112 | 113 | 114 | true 115 | 116 | 117 | 118 | 119 | true 120 | 121 | 122 | 123 | 0 124 | 0 125 | 421 126 | 251 127 | 128 | 129 | 130 | PointingHandCursor 131 | 132 | 133 | true 134 | 135 | 136 | false 137 | 138 | 139 | border:none; opacity: 0%; background: transparent; 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 0 150 | 610 151 | 480 152 | 40 153 | 154 | 155 | 156 | 157 | Montserrat 158 | false 159 | true 160 | 161 | 162 | 163 | border: none; 164 | border-radius: none; 165 | background: none; 166 | color: #ecedec; 167 | 168 | 169 | 170 | 1 171 | 172 | 173 | <html><head/><body><p align="center">Copyright © 2022 <span style=" font-weight:600;">Comepress</span> <br/> Made by <span style=" font-weight:600;">Nayam Amarshe</span> with ⌨ and 🖱</p></body></html> 174 | 175 | 176 | 177 | 178 | 179 | 170 180 | 490 181 | 151 182 | 28 183 | 184 | 185 | 186 | 187 | 0 188 | 0 189 | 190 | 191 | 192 | QCheckBox { 193 | border: none; 194 | background:none; 195 | color: #ecedec; 196 | } 197 | 198 | QCheckBox::indicator { 199 | width: 18px; 200 | height: 18px; 201 | } 202 | 203 | QCheckBox::indicator:checked { 204 | image: url("res/checked.svg") 205 | } 206 | 207 | QCheckBox::indicator:unchecked { 208 | image: url("res/unchecked.svg") 209 | } 210 | 211 | 212 | 213 | Backup Original Files 214 | 215 | 216 | true 217 | 218 | 219 | false 220 | 221 | 222 | false 223 | 224 | 225 | 226 | 227 | 228 | 0 229 | 10 230 | 480 231 | 35 232 | 233 | 234 | 235 | border: none; 236 | border-radius: none; 237 | background: none; 238 | border-image: url(res/logo.png); 239 | 240 | 241 | 242 | 243 | 244 | 450 245 | 15 246 | 16 247 | 16 248 | 249 | 250 | 251 | QPushButton { 252 | font-weight: bold; 253 | background: #2a2c2e; 254 | border: none; 255 | border-radius: 7px; 256 | background: #ff0901; 257 | } 258 | 259 | QPushButton:hover{ 260 | background: #A40005; 261 | } 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 425 271 | 15 272 | 16 273 | 16 274 | 275 | 276 | 277 | QPushButton { 278 | font-weight: bold; 279 | background: #feba41; 280 | border: none; 281 | border-radius: 7px; 282 | } 283 | 284 | QPushButton:hover { 285 | background: #C08D31; 286 | } 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 140 296 | 440 297 | 221 298 | 41 299 | 300 | 301 | 302 | 303 | 304 | 305 | border: none; 306 | color: #fff; 307 | 308 | 309 | Quality 310 | 311 | 312 | 313 | 314 | 315 | 316 | QSlider { 317 | border: none; 318 | border-radius:5px; 319 | } 320 | QSlider::groove:horizontal { 321 | height: 10px; 322 | background: #404040; 323 | border-radius:5px; 324 | } 325 | 326 | QSlider::handle:horizontal { 327 | background: #7e22ce; 328 | width: 18px; 329 | margin: -4px 0; 330 | border-radius: 9px; 331 | } 332 | 333 | QSlider::add-page:horizontal { 334 | background: #404040; 335 | border-radius:5px; 336 | 337 | } 338 | 339 | QSlider::sub-page:horizontal { 340 | background: #7e22ce; 341 | border-radius:5px; 342 | 343 | } 344 | 345 | 346 | 1 347 | 348 | 349 | 100 350 | 351 | 352 | 100 353 | 354 | 355 | Qt::Horizontal 356 | 357 | 358 | 359 | 360 | 361 | 362 | border: none; 363 | color: #fff; 364 | 365 | 366 | 90 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 140 376 | 400 377 | 221 378 | 41 379 | 380 | 381 | 382 | 383 | 384 | 385 | border: none; 386 | border-radius: none; 387 | background: none; 388 | color: #ecedec; 389 | 390 | 391 | Output Format 392 | 393 | 394 | 395 | 396 | 397 | 398 | QComboBox{ 399 | background: #28262b; 400 | border: 2px solid #65606C; 401 | color: #fff; 402 | border-radius:6px; 403 | } 404 | 405 | 406 | 407 | JPG 408 | 409 | 410 | 411 | 412 | PNG 413 | 414 | 415 | 416 | 417 | WEBP 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | -------------------------------------------------------------------------------- /res/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NayamAmarshe/comepress/f688fca431b9d1bd89adb3157757496dae51e1ba/res/folder.png -------------------------------------------------------------------------------- /res/inbox_tray_3d.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NayamAmarshe/comepress/f688fca431b9d1bd89adb3157757496dae51e1ba/res/inbox_tray_3d.ico -------------------------------------------------------------------------------- /res/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NayamAmarshe/comepress/f688fca431b9d1bd89adb3157757496dae51e1ba/res/logo.png -------------------------------------------------------------------------------- /res/unchecked.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------