mirror of
https://github.com/Huluti/Curtail.git
synced 2024-09-11 14:16:12 +03:00
Compare commits
5 Commits
cb2c3dbbe3
...
ad88361278
Author | SHA1 | Date | |
---|---|---|---|
|
ad88361278 | ||
|
850f35a630 | ||
|
7370e5a1f7 | ||
|
85c25fffdb | ||
|
54be29dc31 |
@ -8,7 +8,8 @@
|
||||
<bug-database rdf:resource="https://github.com/Huluti/Curtail/issues" />
|
||||
|
||||
<programming-language>Python</programming-language>
|
||||
<platform>GTK 3</platform>
|
||||
<platform>GTK 4</platform>
|
||||
<platform>Libadwaita</platform>
|
||||
|
||||
<maintainer>
|
||||
<foaf:Person>
|
||||
|
@ -1,7 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gresources>
|
||||
<gresource prefix="/com/github/huluti/Curtail/">
|
||||
<file alias="gtk/help-overlay.ui">ui/help_overlay.ui</file>
|
||||
</gresource>
|
||||
<gresource prefix="/com/github/huluti/Curtail/ui">
|
||||
<file alias="help_overlay.ui">ui/help_overlay.ui</file>
|
||||
<file alias="preferences.ui">ui/preferences.ui</file>
|
||||
<file alias="menu.ui">ui/menu.ui</file>
|
||||
<file alias="window.ui">ui/window.ui</file>
|
||||
|
@ -94,11 +94,11 @@
|
||||
<property name="margin-end">10</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="margin-bottom">10</property>
|
||||
<property name="hexpand">true</property>
|
||||
<property name="selection-mode">none</property>
|
||||
<style>
|
||||
<class name="boxed-list" />
|
||||
</style>
|
||||
<property name="hexpand">true</property>
|
||||
<property name="selection-mode">none</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
@ -1,10 +1,10 @@
|
||||
data/com.github.huluti.Curtail.appdata.xml.in
|
||||
data/com.github.huluti.Curtail.desktop.in
|
||||
data/com.github.huluti.Curtail.gschema.xml
|
||||
src/ui/help_overlay.ui
|
||||
src/ui/preferences.ui
|
||||
src/ui/menu.ui
|
||||
src/ui/window.ui
|
||||
data/ui/help_overlay.ui
|
||||
data/ui/preferences.ui
|
||||
data/ui/menu.ui
|
||||
data/ui/window.ui
|
||||
src/compressor.py
|
||||
src/main.py
|
||||
src/preferences.py
|
||||
|
@ -15,13 +15,14 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import threading
|
||||
import subprocess
|
||||
from gi.repository import Gtk, Gio, GObject, GLib
|
||||
from gi.repository import Gtk, GLib, Gio, GObject
|
||||
from shutil import copy2
|
||||
from pathlib import Path
|
||||
|
||||
from .resultitem import ResultItem
|
||||
from .tools import message_dialog, get_file_type, sizeof_fmt
|
||||
from .tools import message_dialog, get_file_type
|
||||
|
||||
|
||||
SETTINGS_SCHEMA = 'com.github.huluti.Curtail'
|
||||
@ -30,72 +31,80 @@ SETTINGS_SCHEMA = 'com.github.huluti.Curtail'
|
||||
class Compressor():
|
||||
_settings = Gio.Settings.new(SETTINGS_SCHEMA)
|
||||
|
||||
def __init__(self, win, filename, new_filename):
|
||||
def __init__(self, files, c_update_results_model, c_update_result_item,
|
||||
c_enable_compression):
|
||||
super().__init__()
|
||||
|
||||
self.win = win
|
||||
self.files = files
|
||||
self.c_update_results_model = c_update_results_model
|
||||
self.c_update_result_item = c_update_result_item
|
||||
self.c_enable_compression = c_enable_compression
|
||||
|
||||
# Filenames
|
||||
self.filename = filename
|
||||
self.new_filename = new_filename
|
||||
self.compression_timeout = self._settings.get_int('compression-timeout')
|
||||
self.lossy = self._settings.get_boolean('lossy')
|
||||
self.metadata = self._settings.get_boolean('metadata')
|
||||
self.file_attributes = self._settings.get_boolean('file-attributes')
|
||||
|
||||
self.file_data = Path(self.filename)
|
||||
self.new_file_data = Path(self.new_filename)
|
||||
def compress_images(self):
|
||||
result_items = []
|
||||
for file in self.files:
|
||||
file_data = Path(file['filename'])
|
||||
full_name = file_data.name
|
||||
size = file_data.stat().st_size
|
||||
|
||||
self.full_name = self.file_data.name
|
||||
result_item = ResultItem(
|
||||
full_name,
|
||||
file['filename'],
|
||||
file['new_filename'],
|
||||
size
|
||||
)
|
||||
|
||||
self.size = self.file_data.stat().st_size
|
||||
result_items.append(result_item)
|
||||
GLib.idle_add(self.c_update_results_model, result_item) # update ui
|
||||
|
||||
self.thread = threading.Thread(target=self._compress_images, args=(result_items,))
|
||||
self.thread.start()
|
||||
|
||||
def _compress_images(self, result_items):
|
||||
for result_item in result_items:
|
||||
file_type = get_file_type(result_item.filename)
|
||||
if file_type:
|
||||
if file_type == 'png':
|
||||
command = self.build_png_command(result_item, self.lossy, self.metadata, self.file_attributes)
|
||||
elif file_type == 'jpg':
|
||||
command = self.build_jpg_command(result_item, self.lossy, self.metadata, self.file_attributes)
|
||||
elif file_type == 'webp':
|
||||
command = self.build_webp_command(result_item, self.lossy, self.metadata)
|
||||
self.run_command(command, result_item) # compress image
|
||||
GLib.idle_add(self.c_enable_compression, result_item) # update ui
|
||||
|
||||
def run_command(self, command, result_item):
|
||||
compression_timeout = self._settings.get_int('compression-timeout')
|
||||
error = False
|
||||
error_message = ''
|
||||
try:
|
||||
subprocess.call(command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
stdin=subprocess.PIPE,
|
||||
shell=True,
|
||||
timeout=compression_timeout)
|
||||
self.command_finished(result_item)
|
||||
timeout=self.compression_timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
message = _("Compression has reached the configured timeout of {} seconds. \
|
||||
You can change it in Preferences.").format(compression_timeout)
|
||||
message_dialog(self.win, _("Timeout expired"), message)
|
||||
result_item.running = False
|
||||
result_item.error = True
|
||||
error_message = _("Compression has reached the configured timeout of {} seconds.").format(self.compression_timeout)
|
||||
error = True
|
||||
except Exception as err:
|
||||
message_dialog(self.win, _("An error has occured"), str(err))
|
||||
result_item.running = False
|
||||
result_item.error = True
|
||||
error_message = _("An unknown error has occured.")
|
||||
error = True
|
||||
finally:
|
||||
self.command_finished(result_item, error, error_message)
|
||||
|
||||
def compress_image(self):
|
||||
result_item = ResultItem(self.full_name, self.filename,
|
||||
self.new_filename, sizeof_fmt(self.size), 0, True, False)
|
||||
self.win.results_model.append(result_item)
|
||||
def command_finished(self, result_item, error, error_message):
|
||||
if not error:
|
||||
new_file_data = Path(result_item.new_filename)
|
||||
result_item.new_size = new_file_data.stat().st_size
|
||||
|
||||
GLib.idle_add(self.command_start, result_item)
|
||||
GLib.idle_add(self.c_update_result_item, result_item, error, error_message) # update ui
|
||||
|
||||
def command_start(self, result_item):
|
||||
lossy = self._settings.get_boolean('lossy')
|
||||
metadata = self._settings.get_boolean('metadata')
|
||||
file_attributes = self._settings.get_boolean('file-attributes')
|
||||
|
||||
file_type = get_file_type(self.filename)
|
||||
if file_type:
|
||||
if file_type == 'png':
|
||||
command = self.build_png_command(lossy, metadata, file_attributes)
|
||||
elif file_type == 'jpg':
|
||||
command = self.build_jpg_command(lossy, metadata, file_attributes)
|
||||
elif file_type == 'webp':
|
||||
command = self.build_webp_command(lossy, metadata)
|
||||
self.run_command(command, result_item) # compress image
|
||||
|
||||
def command_finished(self, result_item):
|
||||
new_size = self.new_file_data.stat().st_size
|
||||
result_item.size = result_item.size + ' -> ' + sizeof_fmt(new_size)
|
||||
result_item.savings = str(round(100 - (new_size * 100 / self.size), 2)) + '%'
|
||||
result_item.running = False
|
||||
|
||||
def build_png_command(self, lossy, metadata, file_attributes):
|
||||
def build_png_command(self, result_item, lossy, metadata, file_attributes):
|
||||
pngquant = 'pngquant --quality=0-{} -f "{}" --output "{}"'
|
||||
optipng = 'optipng -clobber -o{} "{}" -out "{}"'
|
||||
|
||||
@ -110,17 +119,17 @@ You can change it in Preferences.").format(compression_timeout)
|
||||
png_lossless_level = self._settings.get_int('png-lossless-level')
|
||||
|
||||
if lossy: # lossy compression
|
||||
command = pngquant.format(png_lossy_level, self.filename,
|
||||
self.new_filename)
|
||||
command = pngquant.format(png_lossy_level, result_item.filename,
|
||||
result_item.new_filename)
|
||||
command += ' && '
|
||||
command += optipng.format(png_lossless_level, self.new_filename,
|
||||
self.new_filename)
|
||||
command += optipng.format(png_lossless_level, result_item.new_filename,
|
||||
result_item.new_filename)
|
||||
else: # lossless compression
|
||||
command = optipng.format(png_lossless_level, self.filename,
|
||||
self.new_filename)
|
||||
command = optipng.format(png_lossless_level, result_item.filename,
|
||||
result_item.new_filename)
|
||||
return command
|
||||
|
||||
def build_jpg_command(self, lossy, metadata, file_attributes):
|
||||
def build_jpg_command(self, result_item, lossy, metadata, file_attributes):
|
||||
do_new_file = self._settings.get_boolean('new-file')
|
||||
do_jpg_progressive = self._settings.get_boolean('jpg-progressive')
|
||||
|
||||
@ -146,19 +155,19 @@ You can change it in Preferences.").format(compression_timeout)
|
||||
jpg_lossy_level = self._settings.get_int('jpg-lossy-level')
|
||||
if lossy: # lossy compression
|
||||
if do_new_file:
|
||||
command = jpegoptim.format(jpg_lossy_level, self.filename,
|
||||
self.new_filename)
|
||||
command = jpegoptim.format(jpg_lossy_level, result_item.filename,
|
||||
result_item.new_filename)
|
||||
else:
|
||||
command = jpegoptim.format(jpg_lossy_level, self.filename)
|
||||
command = jpegoptim.format(jpg_lossy_level, result_item.filename)
|
||||
else: # lossless compression
|
||||
if do_new_file:
|
||||
command = jpegoptim2.format(self.filename, self.new_filename)
|
||||
command = jpegoptim2.format(result_item.filename, result_item.new_filename)
|
||||
else:
|
||||
command = jpegoptim2.format(self.filename)
|
||||
command = jpegoptim2.format(result_item.filename)
|
||||
return command
|
||||
|
||||
def build_webp_command(self, lossy, metadata):
|
||||
command = "cwebp " + self.filename
|
||||
def build_webp_command(self, result_item, lossy, metadata):
|
||||
command = "cwebp " + result_item.filename
|
||||
|
||||
# cwebp doesn't preserve any metadata by default
|
||||
if metadata:
|
||||
@ -175,7 +184,7 @@ You can change it in Preferences.").format(compression_timeout)
|
||||
# multithreaded, (lossless) compression mode, quality, output
|
||||
command += " -mt -m {}".format(compression_level)
|
||||
command += " -q {}".format(quality)
|
||||
command += " -o {}".format(self.new_filename)
|
||||
command += " -o {}".format(result_item.new_filename)
|
||||
|
||||
return command
|
||||
|
||||
|
@ -2,26 +2,28 @@
|
||||
|
||||
from gi.repository import GObject
|
||||
|
||||
from .tools import sizeof_fmt
|
||||
|
||||
class ResultItem(GObject.Object):
|
||||
name = GObject.Property(type=str)
|
||||
filename = GObject.Property(type=str)
|
||||
new_filename = GObject.Property(type=str)
|
||||
size = GObject.Property(type=str)
|
||||
savings = GObject.Property(type=str)
|
||||
size = GObject.Property(type=int)
|
||||
new_size = GObject.Property(type=int, default=0)
|
||||
subtitle_label = GObject.Property(type=str, default='')
|
||||
savings = GObject.Property(type=str, default='')
|
||||
running = GObject.Property(type=bool, default=True)
|
||||
error = GObject.Property(type=bool, default=False)
|
||||
error_message = GObject.Property(type=str, default='')
|
||||
|
||||
def __init__(self, name, filename, new_filename, size, savings, running,
|
||||
error):
|
||||
def __init__(self, name, filename, new_filename, size):
|
||||
super().__init__()
|
||||
|
||||
self.name = name
|
||||
self.filename = filename
|
||||
self.new_filename = new_filename
|
||||
self.size = size
|
||||
self.savings = savings
|
||||
self.running = running
|
||||
self.error = error
|
||||
self.subtitle_label = sizeof_fmt(size)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.name)
|
||||
|
@ -24,7 +24,7 @@ from .resultitem import ResultItem
|
||||
from .preferences import CurtailPrefsWindow
|
||||
from .compressor import Compressor
|
||||
from .tools import message_dialog, add_filechooser_filters, get_file_type, \
|
||||
create_image_from_file
|
||||
create_image_from_file, sizeof_fmt
|
||||
|
||||
CURTAIL_PATH = '/com/github/huluti/Curtail/'
|
||||
SETTINGS_SCHEMA = 'com.github.huluti.Curtail'
|
||||
@ -88,7 +88,6 @@ class CurtailWindow(Gtk.ApplicationWindow):
|
||||
|
||||
# Results
|
||||
self.listbox.bind_model(self.results_model, self.create_result_row)
|
||||
self.adjustment = self.scrolled_window.get_vadjustment()
|
||||
|
||||
def create_simple_action(self, action_name, callback, shortcut=None):
|
||||
action = Gio.SimpleAction.new(action_name, None)
|
||||
@ -104,6 +103,10 @@ class CurtailWindow(Gtk.ApplicationWindow):
|
||||
self.create_simple_action('about', self.on_about)
|
||||
self.create_simple_action('quit', self.on_quit, '<Primary>q')
|
||||
|
||||
def enable_compression(self, enable):
|
||||
self.filechooser_button_headerbar.set_sensitive(enable)
|
||||
self.clear_button_headerbar.set_sensitive(enable)
|
||||
|
||||
def show_results(self, show):
|
||||
if show:
|
||||
self.homebox.set_visible(False)
|
||||
@ -115,17 +118,32 @@ class CurtailWindow(Gtk.ApplicationWindow):
|
||||
self.clear_button_headerbar.set_visible(False)
|
||||
|
||||
def go_end_scrolledwindow(self):
|
||||
self.adjustment.set_value(self.adjustment.get_upper())
|
||||
adjustment = self.scrolled_window.get_vadjustment()
|
||||
adjustment.set_value(adjustment.get_upper() - adjustment.get_page_size())
|
||||
self.scrolled_window.set_vadjustment(adjustment)
|
||||
|
||||
def clear_results(self, *args):
|
||||
self.show_results(False)
|
||||
self.results_model.remove_all()
|
||||
|
||||
def update_results_model(self, result_item):
|
||||
self.results_model.append(result_item)
|
||||
self.go_end_scrolledwindow()
|
||||
|
||||
def update_result_item(self, result_item, error, error_message):
|
||||
result_item.running = False
|
||||
result_item.error = error
|
||||
if not error:
|
||||
result_item.savings = str(round(100 - (result_item.new_size * 100 / result_item.size), 2)) + '%'
|
||||
result_item.subtitle_label += ' -> ' + sizeof_fmt(result_item.new_size)
|
||||
else:
|
||||
result_item.subtitle_label = error_message
|
||||
|
||||
def create_result_row(self, result_item):
|
||||
row = Adw.ActionRow()
|
||||
row.set_title(result_item.name)
|
||||
row.set_tooltip_text(result_item.new_filename)
|
||||
row.set_subtitle(result_item.size)
|
||||
row.set_subtitle(result_item.subtitle_label)
|
||||
|
||||
image = create_image_from_file(result_item.filename, 60, 60)
|
||||
row.add_prefix(image)
|
||||
@ -145,7 +163,7 @@ class CurtailWindow(Gtk.ApplicationWindow):
|
||||
|
||||
result_item.bind_property('savings', savings_widget, 'label',
|
||||
GObject.BindingFlags.DEFAULT)
|
||||
result_item.bind_property('size', row, 'subtitle',
|
||||
result_item.bind_property('subtitle_label', row, 'subtitle',
|
||||
GObject.BindingFlags.DEFAULT)
|
||||
result_item.bind_property('running', spinner, 'visible',
|
||||
GObject.BindingFlags.DEFAULT)
|
||||
@ -290,13 +308,11 @@ class CurtailWindow(Gtk.ApplicationWindow):
|
||||
|
||||
def compress_images(self, files):
|
||||
self.show_results(True)
|
||||
self.enable_compression(False)
|
||||
|
||||
for file in files:
|
||||
# Call compressor
|
||||
compressor = Compressor(self, file['filename'],
|
||||
file['new_filename'])
|
||||
compressor.compress_image()
|
||||
self.go_end_scrolledwindow()
|
||||
compressor = Compressor(files, self.update_results_model,
|
||||
self.update_result_item, self.enable_compression)
|
||||
GLib.idle_add(compressor.compress_images)
|
||||
|
||||
def on_lossy_changed(self, switch, state):
|
||||
self._settings.set_boolean('lossy', switch.get_active())
|
||||
|
Loading…
Reference in New Issue
Block a user