Compare commits

...

5 Commits

Author SHA1 Message Date
Hugo Posnic
ad88361278 Do compression in a separate thread 2023-03-31 10:43:50 +02:00
Hugo Posnic
850f35a630 Update some paths 2023-03-31 10:13:30 +02:00
Hugo Posnic
7370e5a1f7 Fix help_overlay 2023-03-31 10:13:15 +02:00
Hugo Posnic
85c25fffdb
Merge pull request #145 from sophie-h/patch-2
meta: Update to GTK 4 in .doap
2023-03-31 10:12:19 +02:00
Sophie Herold
54be29dc31
meta: Update to GTK 4 in .doap 2023-03-30 21:06:53 +02:00
7 changed files with 120 additions and 90 deletions

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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())