This commit is contained in:
mikhailnov 2017-07-09 10:18:37 +00:00 committed by GitHub
commit b9c630ad2d
37 changed files with 1830 additions and 17 deletions

2
README
View file

@ -4,7 +4,7 @@ A cross-platform tool for optimizing PNG and JPG files.
Trimage is a cross-platform GUI and command-line interface to optimize image
files via "optipng":http://optipng.sourceforge.net/,
"advpng":http://advancemame.sourceforge.net/comp-readme.html and
"jpegoptim":http://www.kokkonen.net/tjko/projects.html, depending on the
"guetzli":https://github.com/google/guetzli, depending on the
filetype (currently, PNG and JPG files are supported). It was inspired by
"imageoptim":http://imageoptim.pornel.net/. All image files are losslessly
compressed on the highest available compression levels. Trimage gives you

7
debian/changelog vendored
View file

@ -1,3 +1,10 @@
trimage (1.1) xenial; urgency=medium
* change jpegoptim to guetzli
* Add Russian translation in trimage.desktop
-- Mikhail Novosyolov <mikhailnov@dumalogiya.ru> Sun, 09 Jul 2017 13:05:00 +0300
trimage (1.0.5-0ubuntu1) jaunty; urgency=low
* prevent images from becoming larger after recompression

9
debian/control vendored
View file

@ -1,21 +1,20 @@
Source: trimage
Section: graphics
Priority: optional
Maintainer: Kilian Valkhof <kilian@kilianvalkhof.com>
Build-Depends: debhelper (>=7), python-support (>=0.8.7), python
Maintainer: Mikhail Novosyolov <mikhailnov@dumalogiya.ru>
Build-Depends: debhelper (>=7), dh-python, python
XS-Python-Version: >=2.6
Standards-Version: 3.9.1
Homepage: http://trimage.org
Package: trimage
Architecture: all
Depends: ${misc:Depends}, ${python:Depends}, python-qt4 (>=4.4), optipng (>=0.6.2.1), advancecomp (>=1.15), jpegoptim (>=1.2.2), pngcrush (>=1.6.7)
Depends: ${misc:Depends}, ${python:Depends}, python-qt4 (>=4.4), optipng (>=0.6.2.1), advancecomp (>=1.15), guetzli, pngcrush (>=1.6.7)
XB-Python-Version: ${python:Versions}
Description: GUI and command-line interface to optimize image files
Trimage is a cross-platform GUI and command-line interface to optimize image
files via optipng, advpng, pngcrush and jpegoptim, depending on the filetype
files via optipng, advpng, pngcrush and guetzli, depending on the filetype
(currently, PNG and JPG files are supported). All image files are losslessly
compressed on the highest available compression levels. Trimage gives you
various input functions to fit your own workflow: A regular file dialog,
dragging and dropping and various command line options.

1
debian/debhelper-build-stamp vendored Normal file
View file

@ -0,0 +1 @@
trimage

1
debian/files vendored Normal file
View file

@ -0,0 +1 @@
trimage_1.1_all.deb graphics optional

3
debian/rules vendored Normal file → Executable file
View file

@ -1,4 +1,3 @@
#!/usr/bin/make -f
%:
dh $@
dh $@ --with python2

20
debian/trimage.debhelper.log vendored Normal file
View file

@ -0,0 +1,20 @@
dh_update_autotools_config
dh_auto_configure
dh_auto_build
dh_auto_test
dh_prep
dh_auto_install
dh_installdocs
dh_installchangelogs
dh_installman
dh_icons
dh_perl
dh_link
dh_strip_nondeterminism
dh_compress
dh_fixperms
dh_installdeb
dh_gencontrol
dh_md5sums
dh_builddeb
dh_builddeb

7
debian/trimage.postinst.debhelper vendored Normal file
View file

@ -0,0 +1,7 @@
# Automatically added by dh_python2:
if which pycompile >/dev/null 2>&1; then
pycompile -p trimage
fi
# End automatically added section

12
debian/trimage.prerm.debhelper vendored Normal file
View file

@ -0,0 +1,12 @@
# Automatically added by dh_python2:
if which pyclean >/dev/null 2>&1; then
pyclean -p trimage
else
dpkg -L trimage | grep \.py$ | while read file
do
rm -f "${file}"[co] >/dev/null
done
fi
# End automatically added section

4
debian/trimage.substvars vendored Normal file
View file

@ -0,0 +1,4 @@
python:Versions=2.7
python:Depends=python:any, python:any (<< 2.8), python:any (>= 2.6~), python:any (>= 2.7.5-5~), python:any (>= 2.7~)
misc:Depends=
misc:Pre-Depends=

17
debian/trimage/DEBIAN/control vendored Normal file
View file

@ -0,0 +1,17 @@
Package: trimage
Version: 1.1
Architecture: all
Maintainer: Mikhail Novosyolov <mikhailnov@dumalogiya.ru>
Installed-Size: 104
Depends: python:any (<< 2.8), python:any (>= 2.7.5-5~), python-qt4 (>= 4.4), optipng (>= 0.6.2.1), advancecomp (>= 1.15), guetzli, pngcrush (>= 1.6.7)
Section: graphics
Priority: optional
Homepage: http://trimage.org
Description: GUI and command-line interface to optimize image files
Trimage is a cross-platform GUI and command-line interface to optimize image
files via optipng, advpng, pngcrush and guetzli, depending on the filetype
(currently, PNG and JPG files are supported). All image files are losslessly
compressed on the highest available compression levels. Trimage gives you
various input functions to fit your own workflow: A regular file dialog,
dragging and dropping and various command line options.
Python-Version: 2.7

19
debian/trimage/DEBIAN/md5sums vendored Normal file
View file

@ -0,0 +1,19 @@
9c1f31a6520e133d3e2f0b74b4bc4fd6 usr/bin/trimage
d06474fbf8d34b279b49d8cad59af2d0 usr/lib/python2.7/dist-packages/trimage-1.0.2.egg-info
750f3239a617e569f77881ac0122a2b9 usr/lib/python2.7/dist-packages/trimage/ThreadPool/ThreadPool.py
8111d2e44229ba4ecba01f59329af7c6 usr/lib/python2.7/dist-packages/trimage/ThreadPool/__init__.py
d41d8cd98f00b204e9800998ecf8427e usr/lib/python2.7/dist-packages/trimage/__init__.py
6440f77c90e6d358295cd62d603db1fb usr/lib/python2.7/dist-packages/trimage/filesize/__init__.py
d40a0e5bd2b317aa568e16b95d514df9 usr/lib/python2.7/dist-packages/trimage/filesize/filesize.py
9f2b6e23b29836684c216224628d4394 usr/lib/python2.7/dist-packages/trimage/pixmaps/compressing.gif
42636a9b4adbaee8d612cfd42f09151f usr/lib/python2.7/dist-packages/trimage/pixmaps/list-add.png
0ce6c6d3aa5abefc70f2abe91bfb9c5a usr/lib/python2.7/dist-packages/trimage/pixmaps/trimage-icon.png
aba26621c9399ce15c365e669b2a06dd usr/lib/python2.7/dist-packages/trimage/pixmaps/view-refresh.png
4a4e4f786b30bb755d30b506d0477a1b usr/lib/python2.7/dist-packages/trimage/trimage.py
cde8c6def70c52b2111563211b8fa007 usr/lib/python2.7/dist-packages/trimage/ui.py
e239b7003fa0407cdd6bf28c6d72c2a3 usr/share/applications/trimage.desktop
e1f82ec7e26a52a491ca6a224eff5769 usr/share/doc/trimage/README
3baf9c47a7390888522dd0fee82bf3d5 usr/share/doc/trimage/changelog.gz
ddaf427836d208b1b7115d9e202b65c2 usr/share/doc/trimage/copyright
d866aefacb2190ce623bacf964ec12ee usr/share/icons/hicolor/scalable/apps/trimage.svg
42768f4574b21cdabf82b98c025137fd usr/share/man/man1/trimage.1.gz

9
debian/trimage/DEBIAN/postinst vendored Executable file
View file

@ -0,0 +1,9 @@
#!/bin/sh
set -e
# Automatically added by dh_python2:
if which pycompile >/dev/null 2>&1; then
pycompile -p trimage
fi
# End automatically added section

14
debian/trimage/DEBIAN/prerm vendored Executable file
View file

@ -0,0 +1,14 @@
#!/bin/sh
set -e
# Automatically added by dh_python2:
if which pyclean >/dev/null 2>&1; then
pyclean -p trimage
else
dpkg -L trimage | grep \.py$ | while read file
do
rm -f "${file}"[co] >/dev/null
done
fi
# End automatically added section

24
debian/trimage/usr/bin/trimage vendored Executable file
View file

@ -0,0 +1,24 @@
#!/usr/bin/python
#coding: utf-8
#
#Copyright (c) 2010 Kilian Valkhof, Paul Chaplin, Tarnay Kálmán
#
#Permission is hereby granted, free of charge, to any person
#obtaining a copy of this software and associated documentation
#files (the "Software"), to deal in the Software without
#restriction, including without limitation the rights to use,
#copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the
#Software is furnished to do so, subject to the following
#conditions:
#
#The above copyright notice and this permission notice shall be
#included in all copies or substantial portions of the Software.
import os, sys
import subprocess
import trimage
if __name__ == "__main__":
path = os.path.join(os.path.dirname(trimage.__file__), "trimage.py")
subprocess.call([sys.executable, path] + sys.argv[1:])

View file

@ -0,0 +1,11 @@
Metadata-Version: 1.1
Name: trimage
Version: 1.0.2
Summary: Trimage image compressor - A cross-platform tool for optimizing PNG and JPG files
Home-page: http://trimage.org
Author: Kilian Valkhof, Paul Chaplin
Author-email: help@trimage.org
License: MIT license
Description: Trimage is a cross-platform GUI and command-line interface to optimize image files via optipng, advpng and guetzli, depending on the filetype (currently, PNG and JPG files are supported). It was inspired by imageoptim. All image files are losslessy compressed on the highest available compression levels. Trimage gives you various input functions to fit your own workflow: A regular file dialog, dragging and dropping and various command line options.
Platform: UNKNOWN
Requires: PyQt4 (>=4.4)

View file

@ -0,0 +1,280 @@
'''
ThreadPool Implementation
@author: Morten Holdflod Moeller - morten@holdflod.dk
@license: LGPL v3
'''
from __future__ import with_statement
from threading import Thread, RLock
from time import sleep
from Queue import Queue, Empty
import logging
import sys
class NullHandler(logging.Handler):
def emit(self, record):
pass
h = sys.stderr
logging.getLogger('threadpool').addHandler(h)
logging.getLogger('threadpool.worker').addHandler(h)
class ThreadPoolMixIn:
"""Mix-in class to handle each request in a new thread from the ThreadPool."""
def __init__(self, threadpool=None):
if (threadpool == None):
threadpool = ThreadPool()
self.__private_threadpool = True
else:
self.__private_threadpool = False
self.__threadpool = threadpool
def process_request_thread(self, request, client_address):
"""Same as in BaseServer but as a thread.
In addition, exception handling is done here.
"""
try:
self.finish_request(request, client_address)
self.close_request(request)
except:
self.handle_error(request, client_address) #IGNORE:W0702
self.close_request(request)
def process_request(self, request, client_address):
self.__threadpool.add_job(self.process_request_thread, [request, client_address])
def shutdown(self):
if (self.__private_threadpool): self.__threadpool.shutdown()
class AddJobException(Exception):
'''
Exceptoion raised when a Job could not be added
to the queue
'''
def __init__(self, msg):
Exception.__init__(self, msg)
class ThreadPool:
'''
The class implementing the ThreadPool.
Instantiate and add jobs using add_job(func, args_list)
'''
class Job: #IGNORE:R0903
'''
Class encapsulating a job to be handled
by ThreadPool workers
'''
def __init__(self, function, args, return_callback=None):
self.callable = function
self.arguments = args
self.return_callback = return_callback
def execute(self):
'''
Called to execute the function
'''
try:
return_value = self.callable(*self.arguments) #IGNORE:W0142
except Exception, excep: #IGNORE:W0703
logger = logging.getLogger("threadpool.worker")
logger.warning("A job in the ThreadPool raised an exception: " + excep)
#else do nothing cause we don't know what to do...
return
try:
if (self.return_callback != None):
self.return_callback(return_value)
except Exception, _: #IGNORE:W0703 everything could go wrong...
logger = logging.getLogger('threadpool')
logger.warning('Error while delivering return value to callback function')
class Worker(Thread):
'''
A worker thread handling jobs in the thread pool
job queue
'''
def __init__(self, pool):
Thread.__init__(self)
if (not isinstance(pool, ThreadPool)):
raise TypeError("pool is not a ThreadPool instance")
self.pool = pool
self.alive = True
self.start()
def run(self):
'''
The workers main-loop getting jobs from queue
and executing them
'''
while self.alive:
#print self.pool.__active_worker_count, self.pool.__worker_count
job = self.pool.get_job()
if (job != None):
self.pool.worker_active()
job.execute()
self.pool.worker_inactive()
else:
self.alive = False
self.pool.punch_out()
def __init__(self, max_workers = 5, kill_workers_after = 3):
if (not isinstance(max_workers, int)):
raise TypeError("max_workers is not an int")
if (max_workers < 1):
raise ValueError('max_workers must be >= 1')
if (not isinstance(kill_workers_after, int)):
raise TypeError("kill_workers_after is not an int")
self.__max_workers = max_workers
self.__kill_workers_after = kill_workers_after
# This Queue is assumed Thread Safe
self.__jobs = Queue()
self.__worker_count_lock = RLock()
self.__worker_count = 0
self.__active_worker_count = 0
self.__shutting_down = False
logger = logging.getLogger('threadpool')
logger.info('started')
def shutdown(self, wait_for_workers_period = 1, clean_shutdown_reties = 5):
if (not isinstance(clean_shutdown_reties, int)):
raise TypeError("clean_shutdown_reties is not an int")
if (not clean_shutdown_reties >= 0):
raise ValueError('clean_shutdown_reties must be >= 0')
if (not isinstance(wait_for_workers_period, int)):
raise TypeError("wait_for_workers_period is not an int")
if (not wait_for_workers_period >= 0):
raise ValueError('wait_for_workers_period must be >= 0')
logger = logging.getLogger("threadpool")
logger.info("shutting down")
with self.__worker_count_lock:
self.__shutting_down = True
self.__max_workers = 0
self.__kill_workers_after = 0
retries_left = clean_shutdown_reties
while (retries_left > 0):
with self.__worker_count_lock:
logger.info("waiting for workers to shut down (%i), %i workers left"%(retries_left, self.__worker_count))
if (self.__worker_count > 0):
retries_left -= 1
else:
retries_left = 0
sleep(wait_for_workers_period)
with self.__worker_count_lock:
if (self.__worker_count > 0):
logger.warning("shutdown stopped waiting. Still %i active workers"%self.__worker_count)
clean_shutdown = False
else:
clean_shutdown = True
logger.info("shutdown complete")
return clean_shutdown
def punch_out(self):
'''
Called by worker to update worker count
when the worker is shutting down
'''
with self.__worker_count_lock:
self.__worker_count -= 1
def __new_worker(self):
'''
Adding a new worker thread to the thread pool
'''
with self.__worker_count_lock:
ThreadPool.Worker(self)
self.__worker_count += 1
def worker_active(self):
with self.__worker_count_lock:
self.__active_worker_count = self.__active_worker_count + 1
def worker_inactive(self):
with self.__worker_count_lock:
self.__active_worker_count = self.__active_worker_count - 1
def add_job(self, function, args = None, return_callback=None):
'''
Put new job into queue
'''
if (not callable(function)):
raise TypeError("function is not a callable")
if (not ( args == None or isinstance(args, list))):
raise TypeError("args is not a list")
if (not (return_callback == None or callable(return_callback))):
raise TypeError("return_callback is not a callable")
if (args == None):
args = []
job = ThreadPool.Job(function, args, return_callback)
with self.__worker_count_lock:
if (self.__shutting_down):
raise AddJobException("ThreadPool is shutting down")
try:
start_new_worker = False
if (self.__worker_count < self.__max_workers):
if (self.__active_worker_count == self.__worker_count):
start_new_worker = True
self.__jobs.put(job)
if (start_new_worker):
self.__new_worker()
except Exception:
raise AddJobException("Could not add job")
def get_job(self):
'''
Retrieve next job from queue
workers die (and should) when
returning None
'''
job = None
try:
if (self.__kill_workers_after < 0):
job = self.__jobs.get(True)
elif (self.__kill_workers_after == 0):
job = self.__jobs.get(False)
else:
job = self.__jobs.get(True, self.__kill_workers_after)
except Empty:
job = None
return job

View file

@ -0,0 +1 @@
from ThreadPool import ThreadPool, ThreadPoolMixIn

View file

@ -0,0 +1,2 @@
from filesize import size
from filesize import traditional, alternative, verbose, iec, si

View file

@ -0,0 +1,115 @@
'''
hurry.filesize
@author: Martijn Faassen, Startifact
@license: ZPL 2.1
'''
traditional = [
(1024 ** 5, 'P'),
(1024 ** 4, 'T'),
(1024 ** 3, 'G'),
(1024 ** 2, 'M'),
(1024 ** 1, 'K'),
(1024 ** 0, 'B'),
]
alternative = [
(1024 ** 5, ' PB'),
(1024 ** 4, ' TB'),
(1024 ** 3, ' GB'),
(1024 ** 2, ' MB'),
(1024 ** 1, ' KB'),
(1024 ** 0, (' byte', ' bytes')),
]
verbose = [
(1024 ** 5, (' petabyte', ' petabytes')),
(1024 ** 4, (' terabyte', ' terabytes')),
(1024 ** 3, (' gigabyte', ' gigabytes')),
(1024 ** 2, (' megabyte', ' megabytes')),
(1024 ** 1, (' kilobyte', ' kilobytes')),
(1024 ** 0, (' byte', ' bytes')),
]
iec = [
(1024 ** 5, 'Pi'),
(1024 ** 4, 'Ti'),
(1024 ** 3, 'Gi'),
(1024 ** 2, 'Mi'),
(1024 ** 1, 'Ki'),
(1024 ** 0, ''),
]
si = [
(1000 ** 5, 'P'),
(1000 ** 4, 'T'),
(1000 ** 3, 'G'),
(1000 ** 2, 'M'),
(1000 ** 1, 'K'),
(1000 ** 0, 'B'),
]
def size(bytes, system=traditional):
"""Human-readable file size.
Using the traditional system, where a factor of 1024 is used::
>>> size(10)
'10B'
>>> size(100)
'100B'
>>> size(1000)
'1000B'
>>> size(2000)
'1K'
>>> size(10000)
'9K'
>>> size(20000)
'19K'
>>> size(100000)
'97K'
>>> size(200000)
'195K'
>>> size(1000000)
'976K'
>>> size(2000000)
'1M'
Using the SI system, with a factor 1000::
>>> size(10, system=si)
'10B'
>>> size(100, system=si)
'100B'
>>> size(1000, system=si)
'1K'
>>> size(2000, system=si)
'2K'
>>> size(10000, system=si)
'10K'
>>> size(20000, system=si)
'20K'
>>> size(100000, system=si)
'100K'
>>> size(200000, system=si)
'200K'
>>> size(1000000, system=si)
'1M'
>>> size(2000000, system=si)
'2M'
"""
for factor, suffix in system:
if bytes >= factor:
break
amount = int(bytes/factor)
if isinstance(suffix, tuple):
singular, multiple = suffix
if amount == 1:
suffix = singular
else:
suffix = multiple
return str(amount) + suffix

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1,570 @@
#!/usr/bin/python
import time
import sys
import errno
from os import listdir
from os import path
from os import remove
from os import access
from os import W_OK as WRITEABLE
from shutil import copy
from subprocess import call, PIPE
from optparse import OptionParser
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from filesize import *
from imghdr import what as determinetype
from Queue import Queue
from ThreadPool import ThreadPool
from multiprocessing import cpu_count
from ui import Ui_trimage
# change version to 1.1 after replacing jpegoptim with guetzli
VERSION = "1.1"
class StartQT4(QMainWindow):
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.ui = Ui_trimage()
self.ui.setupUi(self)
self.showapp = True
self.verbose = True
self.imagelist = []
QCoreApplication.setOrganizationName("Kilian Valkhof")
QCoreApplication.setOrganizationDomain("trimage.org")
QCoreApplication.setApplicationName("Trimage")
self.settings = QSettings()
self.restoreGeometry(self.settings.value("geometry").toByteArray())
# check if apps are installed
if self.checkapps():
quit()
#add quit shortcut
if hasattr(QKeySequence, "Quit"):
self.quit_shortcut = QShortcut(QKeySequence(QKeySequence.Quit),
self)
else:
self.quit_shortcut = QShortcut(QKeySequence("Ctrl+Q"), self)
# disable recompress
self.ui.recompress.setEnabled(False)
#self.ui.recompress.hide()
# make a worker thread
self.thread = Worker()
# connect signals with slots
QObject.connect(self.ui.addfiles, SIGNAL("clicked()"),
self.file_dialog)
QObject.connect(self.ui.recompress, SIGNAL("clicked()"),
self.recompress_files)
QObject.connect(self.quit_shortcut, SIGNAL("activated()"),
qApp, SLOT('quit()'))
QObject.connect(self.ui.processedfiles, SIGNAL("fileDropEvent"),
self.file_drop)
QObject.connect(self.thread, SIGNAL("finished()"), self.update_table)
QObject.connect(self.thread, SIGNAL("terminated()"), self.update_table)
QObject.connect(self.thread, SIGNAL("updateUi"), self.update_table)
self.compressing_icon = QIcon(QPixmap(self.ui.get_image("pixmaps/compressing.gif")))
# activate command line options
self.commandline_options()
if QSystemTrayIcon.isSystemTrayAvailable() and not self.cli:
self.systemtray = Systray(self)
def commandline_options(self):
self.cli = False
"""Set up the command line options."""
parser = OptionParser(version="%prog " + VERSION,
description="GUI front-end to compress png and jpg images via "
"optipng, advpng and guetzli")
parser.set_defaults(verbose=True)
parser.add_option("-v", "--verbose", action="store_true",
dest="verbose", help="Verbose mode (default)")
parser.add_option("-q", "--quiet", action="store_false",
dest="verbose", help="Quiet mode")
parser.add_option("-f", "--file", action="store", type="string",
dest="filename", help="compresses image and exit")
parser.add_option("-d", "--directory", action="store", type="string",
dest="directory", help="compresses images in directory and exit")
options, args = parser.parse_args()
# make sure we quit after processing finished if using cli
if options.filename or options.directory:
QObject.connect(self.thread, SIGNAL("finished()"), quit)
self.cli = True
# send to correct function
if options.filename:
self.file_from_cmd(options.filename.decode("utf-8"))
if options.directory:
self.dir_from_cmd(options.directory.decode("utf-8"))
self.verbose = options.verbose
"""
Input functions
"""
def dir_from_cmd(self, directory):
"""
Read the files in the directory and send all files to compress_file.
"""
self.showapp = False
dirpath = path.abspath(directory)
imagedir = listdir(directory)
filelist = [path.join(dirpath, image) for image in imagedir]
self.delegator(filelist)
def file_from_cmd(self, image):
"""Get the file and send it to compress_file"""
self.showapp = False
filelist = [path.abspath(image)]
self.delegator(filelist)
def file_drop(self, images):
"""
Get a file from the drag and drop handler and send it to compress_file.
"""
self.delegator(images)
def file_dialog(self):
"""Open a file dialog and send the selected images to compress_file."""
fd = QFileDialog(self)
fd.restoreState(self.settings.value("fdstate").toByteArray())
directory = self.settings.value("directory", QVariant("")).toString()
fd.setDirectory(directory)
images = fd.getOpenFileNames(self,
"Select one or more image files to compress",
directory,
# this is a fix for file dialog differentiating between cases
"Image files (*.png *.jpg *.jpeg *.PNG *.JPG *.JPEG)")
self.settings.setValue("fdstate", QVariant(fd.saveState()))
if images:
self.settings.setValue("directory", QVariant(path.dirname(unicode(images[0]))))
self.delegator([unicode(fullpath) for fullpath in images])
def recompress_files(self):
"""Send each file in the current file list to compress_file again."""
self.delegator([row.image.fullpath for row in self.imagelist])
"""
Compress functions
"""
def delegator(self, images):
"""
Receive all images, check them and send them to the worker thread.
"""
delegatorlist = []
for fullpath in images:
try: # recompress images already in the list
image = (i.image for i in self.imagelist
if i.image.fullpath == fullpath).next()
if image.compressed:
image.reset()
image.recompression = True
delegatorlist.append(image)
except StopIteration:
if not path.isdir(fullpath):
self. add_image(fullpath, delegatorlist)
else:
self.walk(fullpath, delegatorlist)
self.update_table()
self.thread.compress_file(delegatorlist, self.showapp, self.verbose,
self.imagelist)
def walk(self, dir, delegatorlist):
"""
Walks a directory, and executes a callback on each file
"""
dir = path.abspath(dir)
for file in [file for file in listdir(dir) if not file in [".","..",".svn",".git",".hg",".bzr",".cvs"]]:
nfile = path.join(dir, file)
if path.isdir(nfile):
self.walk(nfile, delegatorlist)
else:
self.add_image(nfile, delegatorlist)
def add_image(self, fullpath, delegatorlist):
"""
Adds an image file to the delegator list and update the tray and the title of the window
"""
image = Image(fullpath)
if image.valid:
delegatorlist.append(image)
self.imagelist.append(ImageRow(image, self.compressing_icon))
if QSystemTrayIcon.isSystemTrayAvailable() and not self.cli:
self.systemtray.trayIcon.setToolTip("Trimage image compressor (" + str(len(self.imagelist)) + " files)")
self.setWindowTitle("Trimage image compressor (" + str(len(self.imagelist)) + " files)")
else:
print >> sys.stderr, u"[error] %s not a supported image file and/or not writeable" % image.fullpath
"""
UI Functions
"""
def update_table(self):
"""Update the table view with the latest file data."""
tview = self.ui.processedfiles
# set table model
tmodel = TriTableModel(self, self.imagelist,
["Filename", "Old Size", "New Size", "Compressed"])
tview.setModel(tmodel)
# set minimum size of table
vh = tview.verticalHeader()
vh.setVisible(False)
# set horizontal header properties
hh = tview.horizontalHeader()
hh.setStretchLastSection(True)
# set all row heights
nrows = len(self.imagelist)
for row in range(nrows):
tview.setRowHeight(row, 25)
# set the second column to be longest
tview.setColumnWidth(0, 300)
# enable recompress button
self.enable_recompress()
"""
Helper functions
"""
def enable_recompress(self):
"""Enable the recompress button."""
self.ui.recompress.setEnabled(True)
if QSystemTrayIcon.isSystemTrayAvailable() and not self.cli:
self.systemtray.recompress.setEnabled(True)
def checkapps(self):
"""Check if the required command line apps exist."""
exe = ".exe" if (sys.platform == "win32") else ""
status = False
# guetzli does the compression job much better than jpegoptim (and much slower)
#retcode = self.safe_call("jpegoptim" + exe + " --version")
#if retcode != 0:
#status = True
#sys.stderr.write("[error] please install jpegoptim")
retcode = self.safe_call("optipng" + exe + " -v")
if retcode != 0:
status = True
sys.stderr.write("[error] please install optipng")
retcode = self.safe_call("advpng" + exe + " --version")
if retcode != 0:
status = True
sys.stderr.write("[error] please install advancecomp")
retcode = self.safe_call("pngcrush" + exe + " -version")
if retcode != 0:
status = True
sys.stderr.write("[error] please install pngcrush")
return status
# guetzli currently does not have neither --version not --help flags
retcode = self.safe_call("guetzli" + exe)
if retcode != 0:
status = True
sys.stderr.write("[error] please install guetzli")
return status
def safe_call(self, command):
""" cross-platform command-line check """
while True:
try:
return call(command, shell=True, stdout=PIPE)
except OSError, e:
if e.errno == errno.EINTR:
continue
else:
raise
def hide_main_window(self):
if self.isVisible():
self.hide()
if QSystemTrayIcon.isSystemTrayAvailable():
self.systemtray.hideMain.setText("&Show window")
else:
self.show()
if QSystemTrayIcon.isSystemTrayAvailable():
self.systemtray.hideMain.setText("&Hide window")
def closeEvent(self, event):
self.settings.setValue("geometry", QVariant(self.saveGeometry()))
event.accept()
class TriTableModel(QAbstractTableModel):
def __init__(self, parent, imagelist, header, *args):
"""
@param parent Qt parent object.
@param imagelist A list of tuples.
@param header A list of strings.
"""
QAbstractTableModel.__init__(self, parent, *args)
self.imagelist = imagelist
self.header = header
def rowCount(self, parent):
"""Count the number of rows."""
return len(self.imagelist)
def columnCount(self, parent):
"""Count the number of columns."""
return len(self.header)
def data(self, index, role):
"""Fill the table with data."""
if not index.isValid():
return QVariant()
elif role == Qt.DisplayRole:
data = self.imagelist[index.row()][index.column()]
return QVariant(data)
elif index.column() == 0 and role == Qt.DecorationRole:
# decorate column 0 with an icon of the image itself
f_icon = self.imagelist[index.row()][4]
return QVariant(f_icon)
else:
return QVariant()
def headerData(self, col, orientation, role):
"""Fill the table headers."""
if orientation == Qt.Horizontal and (role == Qt.DisplayRole or
role == Qt.DecorationRole):
return QVariant(self.header[col])
return QVariant()
class ImageRow:
def __init__(self, image, waitingIcon=None):
""" Build the information visible in the table image row. """
self.image = image
d = {
'shortname': lambda i: self.statusStr() % i.shortname,
'oldfilesizestr': lambda i: size(i.oldfilesize, system=alternative)
if i.compressed else "",
'newfilesizestr': lambda i: size(i.newfilesize, system=alternative)
if i.compressed else "",
'ratiostr': lambda i:
"%.1f%%" % (100 - (float(i.newfilesize) / i.oldfilesize * 100))
if i.compressed else "",
'icon': lambda i: i.icon if i.compressed else waitingIcon,
'fullpath': lambda i: i.fullpath, #only used by cli
}
names = ['shortname', 'oldfilesizestr', 'newfilesizestr',
'ratiostr', 'icon']
for i, n in enumerate(names):
d[i] = d[n]
self.d = d
def statusStr(self):
""" Set the status message. """
if self.image.failed:
return "ERROR: %s"
if self.image.compressing:
message = "Compressing %s..."
return message
if not self.image.compressed and self.image.recompression:
return "Queued for recompression..."
if not self.image.compressed:
return "Queued..."
return "%s"
def __getitem__(self, key):
return self.d[key](self.image)
class Image:
def __init__(self, fullpath):
""" gather image information. """
self.valid = False
self.reset()
self.fullpath = fullpath
if path.isfile(self.fullpath) and access(self.fullpath, WRITEABLE):
self.filetype = determinetype(self.fullpath)
if self.filetype in ["jpeg", "png"]:
oldfile = QFileInfo(self.fullpath)
self.shortname = oldfile.fileName()
self.oldfilesize = oldfile.size()
self.icon = QIcon(self.fullpath)
self.valid = True
#def _determinetype(self):
# """ Determine the filetype of the file using imghdr. """
# filetype = determinetype(self.fullpath)
# if filetype in ["jpeg", "png"]:
# self.filetype = filetype
# else:
# self.filetype = None
# return self.filetype
def reset(self):
self.failed = False
self.compressed = False
self.compressing = False
self.recompression = False
def compress(self):
""" Compress the image and return it to the thread. """
if not self.valid:
raise "Tried to compress invalid image (unsupported format or not \
file)"
self.reset()
self.compressing = True
exe = ".exe" if (sys.platform == "win32") else ""
runString = {
# "jpeg": u"jpegoptim" + exe + " -f --strip-all '%(file)s'",
# I commented out jpegoptim because guetzli compresses MUCH BETTER having a similar output quality
# the default quality level for guetzli, if the parameter '--quality Q' is not passed, it 95.
"jpeg": u"guetzli" + exe + " --quality 95 '%(file)s' '%(file)s.bak' && mv '%(file)s.bak' '%(file)s'",
"png": u"optipng" + exe + " -force -o7 '%(file)s'&&advpng" + exe + " -z4 '%(file)s' && pngcrush -rem gAMA -rem alla -rem cHRM -rem iCCP -rem sRGB -rem time '%(file)s' '%(file)s.bak' && mv '%(file)s.bak' '%(file)s'"
}
# Create a backup file
copy(self.fullpath, self.fullpath + '~')
try:
retcode = call(runString[self.filetype] % {"file": self.fullpath},
shell=True, stdout=PIPE)
except:
retcode = -1
if retcode == 0:
self.newfilesize = QFile(self.fullpath).size()
self.compressed = True
# Checks the new file and copy the backup
if self.newfilesize >= self.oldfilesize:
copy(self.fullpath + '~', self.fullpath)
self.newfilesize = self.oldfilesize
# Removes the backup file
remove(self.fullpath + '~')
else:
self.failed = True
self.compressing = False
self.retcode = retcode
return self
class Worker(QThread):
def __init__(self, parent=None):
QThread.__init__(self, parent)
self.toDisplay = Queue()
self.threadpool = ThreadPool(max_workers=cpu_count())
def __del__(self):
self.threadpool.shutdown()
def compress_file(self, images, showapp, verbose, imagelist):
"""Start the worker thread."""
for image in images:
#FIXME:http://code.google.com/p/pythonthreadpool/issues/detail?id=5
time.sleep(0.05)
self.threadpool.add_job(image.compress, None,
return_callback=self.toDisplay.put)
self.showapp = showapp
self.verbose = verbose
self.imagelist = imagelist
self.start()
def run(self):
"""Compress the given file, get data from it and call update_table."""
tp = self.threadpool
while self.showapp or not (tp._ThreadPool__active_worker_count == 0 and
tp._ThreadPool__jobs.empty()):
image = self.toDisplay.get()
self.emit(SIGNAL("updateUi"))
if not self.showapp and self.verbose: # we work via the commandline
if image.retcode == 0:
ir = ImageRow(image)
print("File: " + ir['fullpath'] + ", Old Size: "
+ ir['oldfilesizestr'] + ", New Size: "
+ ir['newfilesizestr'] + ", Ratio: " + ir['ratiostr'])
else:
print >> sys.stderr, u"[error] %s could not be compressed" % image.fullpath
class Systray(QWidget):
def __init__(self, parent):
QWidget.__init__(self)
self.parent = parent
self.createActions()
self.createTrayIcon()
self.trayIcon.show()
def createActions(self):
self.quitAction = QAction(self.tr("&Quit"), self)
QObject.connect(self.quitAction, SIGNAL("triggered()"),
qApp, SLOT("quit()"))
self.addFiles = QAction(self.tr("&Add and compress"), self)
icon = QIcon()
icon.addPixmap(QPixmap(self.parent.ui.get_image(("pixmaps/list-add.png"))),
QIcon.Normal, QIcon.Off)
self.addFiles.setIcon(icon)
QObject.connect(self.addFiles, SIGNAL("triggered()"), self.parent.file_dialog)
self.recompress = QAction(self.tr("&Recompress"), self)
icon2 = QIcon()
icon2.addPixmap(QPixmap(self.parent.ui.get_image(("pixmaps/view-refresh.png"))),
QIcon.Normal, QIcon.Off)
self.recompress.setIcon(icon2)
self.recompress.setDisabled(True)
QObject.connect(self.addFiles, SIGNAL("triggered()"), self.parent.recompress_files)
self.hideMain = QAction(self.tr("&Hide window"), self)
QObject.connect(self.hideMain, SIGNAL("triggered()"), self.parent.hide_main_window)
def createTrayIcon(self):
self.trayIconMenu = QMenu(self)
self.trayIconMenu.addAction(self.addFiles)
self.trayIconMenu.addAction(self.recompress)
self.trayIconMenu.addSeparator()
self.trayIconMenu.addAction(self.hideMain)
self.trayIconMenu.addSeparator()
self.trayIconMenu.addAction(self.quitAction)
if QSystemTrayIcon.isSystemTrayAvailable():
self.trayIcon = QSystemTrayIcon(self)
self.trayIcon.setContextMenu(self.trayIconMenu)
self.trayIcon.setToolTip("Trimage image compressor")
self.trayIcon.setIcon(QIcon(self.parent.ui.get_image("pixmaps/trimage-icon.png")))
if __name__ == "__main__":
app = QApplication(sys.argv)
myapp = StartQT4()
if myapp.showapp:
myapp.show()
sys.exit(app.exec_())

View file

@ -0,0 +1,166 @@
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from os import path
class TrimageTableView(QTableView):
"""Init the table drop event."""
def __init__(self, parent=None):
super(TrimageTableView, self).__init__(parent)
self.setAcceptDrops(True)
def dragEnterEvent(self, event):
if event.mimeData().hasUrls:
event.accept()
else:
event.ignore()
def dragMoveEvent(self, event):
event.accept()
def dropEvent(self, event):
event.accept()
filelist = []
for url in event.mimeData().urls():
filelist.append(unicode(url.toLocalFile()))
self.emit(SIGNAL("fileDropEvent"), (filelist))
class Ui_trimage(object):
def get_image(self, image):
""" Get the correct link to the images used in the UI """
imagelink = path.join(path.dirname(path.dirname(path.realpath(__file__))), "trimage/" + image)
return imagelink
def setupUi(self, trimage):
""" Setup the entire UI """
trimage.setObjectName("trimage")
trimage.resize(600, 170)
trimageIcon = QIcon(self.get_image("pixmaps/trimage-icon.png"))
trimage.setWindowIcon(trimageIcon)
self.centralwidget = QWidget(trimage)
self.centralwidget.setObjectName("centralwidget")
self.gridLayout_2 = QGridLayout(self.centralwidget)
self.gridLayout_2.setMargin(0)
self.gridLayout_2.setSpacing(0)
self.gridLayout_2.setObjectName("gridLayout_2")
self.widget = QWidget(self.centralwidget)
self.widget.setEnabled(True)
sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(1)
sizePolicy.setVerticalStretch(1)
sizePolicy.setHeightForWidth(
self.widget.sizePolicy().hasHeightForWidth())
self.widget.setSizePolicy(sizePolicy)
self.widget.setObjectName("widget")
self.verticalLayout = QVBoxLayout(self.widget)
self.verticalLayout.setSpacing(0)
self.verticalLayout.setMargin(0)
self.verticalLayout.setObjectName("verticalLayout")
self.frame = QFrame(self.widget)
self.frame.setObjectName("frame")
self.verticalLayout_2 = QVBoxLayout(self.frame)
self.verticalLayout_2.setSpacing(0)
self.verticalLayout_2.setMargin(0)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.horizontalLayout = QHBoxLayout()
self.horizontalLayout.setSpacing(0)
self.horizontalLayout.setMargin(10)
self.horizontalLayout.setObjectName("horizontalLayout")
self.addfiles = QPushButton(self.frame)
font = QFont()
font.setPointSize(9)
self.addfiles.setFont(font)
self.addfiles.setCursor(Qt.PointingHandCursor)
icon = QIcon()
icon.addPixmap(QPixmap(self.get_image("pixmaps/list-add.png")), QIcon.Normal, QIcon.Off)
self.addfiles.setIcon(icon)
self.addfiles.setObjectName("addfiles")
self.addfiles.setAcceptDrops(True)
self.horizontalLayout.addWidget(self.addfiles)
self.label = QLabel(self.frame)
font = QFont()
font.setPointSize(8)
self.label.setFont(font)
self.label.setFrameShadow(QFrame.Plain)
self.label.setMargin(1)
self.label.setIndent(10)
self.label.setObjectName("label")
self.horizontalLayout.addWidget(self.label)
spacerItem = QSpacerItem(498, 20, QSizePolicy.Expanding,
QSizePolicy.Minimum)
self.horizontalLayout.addItem(spacerItem)
self.recompress = QPushButton(self.frame)
font = QFont()
font.setPointSize(9)
self.recompress.setFont(font)
self.recompress.setCursor(Qt.PointingHandCursor)
icon1 = QIcon()
icon1.addPixmap(QPixmap(self.get_image("pixmaps/view-refresh.png")), QIcon.Normal, QIcon.Off)
self.recompress.setIcon(icon1)
self.recompress.setCheckable(False)
self.recompress.setObjectName("recompress")
self.horizontalLayout.addWidget(self.recompress)
self.verticalLayout_2.addLayout(self.horizontalLayout)
self.processedfiles = TrimageTableView(self.frame)
self.processedfiles.setEnabled(True)
self.processedfiles.setFrameShape(QFrame.NoFrame)
self.processedfiles.setFrameShadow(QFrame.Plain)
self.processedfiles.setLineWidth(0)
self.processedfiles.setMidLineWidth(0)
self.processedfiles.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.processedfiles.setTabKeyNavigation(True)
self.processedfiles.setAlternatingRowColors(True)
self.processedfiles.setTextElideMode(Qt.ElideRight)
self.processedfiles.setShowGrid(True)
self.processedfiles.setGridStyle(Qt.NoPen)
self.processedfiles.setSortingEnabled(False)
self.processedfiles.setObjectName("processedfiles")
self.processedfiles.resizeColumnsToContents()
self.processedfiles.setSelectionMode(QAbstractItemView.NoSelection)
self.verticalLayout_2.addWidget(self.processedfiles)
self.verticalLayout.addWidget(self.frame)
self.gridLayout_2.addWidget(self.widget, 0, 0, 1, 1)
trimage.setCentralWidget(self.centralwidget)
self.retranslateUi(trimage)
QMetaObject.connectSlotsByName(trimage)
def retranslateUi(self, trimage):
""" Fill in the texts for all UI elements """
trimage.setWindowTitle(QApplication.translate("trimage",
"Trimage image compressor", None, QApplication.UnicodeUTF8))
self.addfiles.setToolTip(QApplication.translate("trimage",
"Add file to the compression list", None,
QApplication.UnicodeUTF8))
self.addfiles.setText(QApplication.translate("trimage",
"&Add and compress", None, QApplication.UnicodeUTF8))
self.addfiles.setShortcut(QApplication.translate("trimage",
"Alt+A", None, QApplication.UnicodeUTF8))
self.label.setText(QApplication.translate("trimage",
"Drag and drop images onto the table", None,
QApplication.UnicodeUTF8))
self.recompress.setToolTip(QApplication.translate("trimage",
"Recompress all images", None, QApplication.UnicodeUTF8))
self.recompress.setText(QApplication.translate("trimage",
"&Recompress", None, QApplication.UnicodeUTF8))
self.recompress.setShortcut(QApplication.translate("trimage",
"Alt+R", None, QApplication.UnicodeUTF8))
self.processedfiles.setToolTip(QApplication.translate("trimage",
"Drag files in here", None, QApplication.UnicodeUTF8))
self.processedfiles.setWhatsThis(QApplication.translate("trimage",
"Drag files in here", None, QApplication.UnicodeUTF8))

View file

@ -0,0 +1,12 @@
[Desktop Entry]
Name=Trimage image compressor
Name[ru]=Программа для уменьшения веса картинок Trimage
Comment=A cross-platform tool for optimizing PNG and JPG files
Comment[ru]=Кросс-платформенная утилита для оптимизации файлов PNG и JPG
Terminal=false
Icon=trimage
Type=Application
Exec=trimage
Categories=Application;Qt;Graphics;
StartupNotify=true

View file

@ -0,0 +1,15 @@
Trimage image compressor
A cross-platform tool for optimizing PNG and JPG files.
Trimage is a cross-platform GUI and command-line interface to optimize image
files via "optipng":http://optipng.sourceforge.net/,
"advpng":http://advancemame.sourceforge.net/comp-readme.html and
"guetzli":https://github.com/google/guetzli, depending on the
filetype (currently, PNG and JPG files are supported). It was inspired by
"imageoptim":http://imageoptim.pornel.net/. All image files are losslessly
compressed on the highest available compression levels. Trimage gives you
various input functions to fit your own workflow: A regular file dialog,
dragging and dropping and various command line options.
Visit "Trimage.org":http://trimage.org for more information

Binary file not shown.

View file

@ -0,0 +1,256 @@
This package was debianized by:
Kilian Valkhof <help@trimage.org> on Tue, 23 Mar 2010 20:18:17 +0100
Upstream Author:
Kilian Valkhof
Copyright:
Copyright (C) 2010 Kilian Valkhof, Paul Chaplin
License:
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
The Debian packaging is:
Copyright (C) 2010 Kilian Valkhof, Paul Chaplin
and is licensed under the MIT license, see above.
hurry.filesize is:
Copyright (C) Martijn Faassen, Startifact
License:
Zope Public License (ZPL) Version 2.1
A copyright notice accompanies this license document that identifies the
copyright holders. This license has been certified as open source. It has
also been designated as GPL compatible by the Free Software Foundation (FSF).
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions in source code must retain the accompanying copyright
notice, this list of conditions, and the following disclaimer.
2. Redistributions in binary form must reproduce the accompanying
copyright notice, this list of conditions, and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
3. Names of the copyright holders must not be used to endorse or promote
products derived from this software without prior written permission from
the copyright holders.
4. The right to distribute this software or to use it for any purpose does
not give you the right to use Servicemarks (sm) or Trademarks (tm) of the
copyright holders. Use of them is covered by separate agreement with the
copyright holders.
5. If any files are modified, you must cause the modified files to carry
prominent notices stating that you changed the files and the date of any
change.
Disclaimer
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY
EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
ThreadPool is:
Copyright (c) Morten Holdflod Moeller
License:
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

View file

@ -0,0 +1,237 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="524.81012"
height="541.92767"
id="svg2"
sodipodi:version="0.32"
inkscape:version="0.46"
sodipodi:docname="logo-trimage.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
version="1.0">
<defs
id="defs4">
<linearGradient
id="linearGradient3207">
<stop
style="stop-color:#ffffff;stop-opacity:0.51401869"
offset="0"
id="stop3209" />
<stop
style="stop-color:#76b9fb;stop-opacity:0;"
offset="1"
id="stop3211" />
</linearGradient>
<linearGradient
id="linearGradient3189">
<stop
id="stop3191"
offset="0"
style="stop-color:#ffffff;stop-opacity:1;" />
<stop
id="stop3193"
offset="1"
style="stop-color:#76b9fb;stop-opacity:0;" />
</linearGradient>
<linearGradient
id="linearGradient3177">
<stop
style="stop-color:#2166ce;stop-opacity:1;"
offset="0"
id="stop3179" />
<stop
style="stop-color:#76b9fb;stop-opacity:1;"
offset="1"
id="stop3181" />
</linearGradient>
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 526.18109 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="744.09448 : 526.18109 : 1"
inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
id="perspective10" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3177"
id="linearGradient3183"
x1="-113.18141"
y1="94.068687"
x2="87.293686"
y2="196.29851"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3189"
id="radialGradient3187"
cx="-65.175232"
cy="257.47021"
fx="-65.175232"
fy="257.47021"
r="147.35561"
gradientTransform="matrix(0.9973701,7.2477038e-2,-8.1815483e-2,1.1258782,20.893644,-31.286202)"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3189"
id="radialGradient3199"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.5134757,2.4437577e-2,-2.7551034e-2,0.5788946,65.578572,65.206227)"
cx="111.03341"
cy="166.65665"
fx="111.03341"
fy="166.65665"
r="147.35561" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3207"
id="radialGradient3205"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.5552136,-0.6613875,0.7169659,0.6018699,37.072783,80.283257)"
cx="56.787258"
cy="73.974876"
fx="56.787258"
fy="73.974876"
r="147.35561" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
gridtolerance="10000"
guidetolerance="10"
objecttolerance="10"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.98994949"
inkscape:cx="299.62219"
inkscape:cy="479.5939"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1180"
inkscape:window-x="0"
inkscape:window-y="0" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-72.611421,-111.20599)">
<path
sodipodi:type="star"
style="fill:url(#linearGradient3183);fill-opacity:1;stroke:none;stroke-width:2.20967627;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path3155"
sodipodi:sides="3"
sodipodi:cx="11.428571"
sodipodi:cy="175.21933"
sodipodi:r1="168.57143"
sodipodi:r2="84.285713"
sodipodi:arg1="1.5707963"
sodipodi:arg2="2.6179939"
inkscape:flatsided="true"
inkscape:rounded="0.11"
inkscape:randomized="0"
d="M 11.428575,343.79076 C -20.688595,343.79076 -150.61715,118.74791 -134.55857,90.933621 C -118.49998,63.119335 141.35712,63.119328 157.41571,90.933613 C 173.47429,118.7479 43.545746,343.79076 11.428575,343.79076 z"
transform="matrix(1.385337,-1.1652199,1.1652199,1.385337,160.77379,177.08667)"
inkscape:export-filename="/home/kilian/workspace/trimage/trimage-icon.png"
inkscape:export-xdpi="11.166932"
inkscape:export-ydpi="11.166932" />
<path
sodipodi:type="star"
style="fill:#ffffff;fill-opacity:0.45833333;stroke:none;stroke-width:3.01696086;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path3157"
sodipodi:sides="3"
sodipodi:cx="11.428571"
sodipodi:cy="175.21933"
sodipodi:r1="168.57143"
sodipodi:r2="84.285713"
sodipodi:arg1="1.5707963"
sodipodi:arg2="2.6179939"
inkscape:flatsided="true"
inkscape:rounded="0.11"
inkscape:randomized="0"
d="M 11.428575,343.79076 C -20.688595,343.79076 -150.61715,118.74791 -134.55857,90.933621 C -118.49998,63.119335 141.35712,63.119328 157.41571,90.933613 C 173.47429,118.7479 43.545746,343.79076 11.428575,343.79076 z"
transform="matrix(1.0146457,-0.853428,0.853428,1.0146457,272.20143,300.96357)"
inkscape:export-filename="/home/kilian/workspace/trimage/trimage-icon.png"
inkscape:export-xdpi="11.166932"
inkscape:export-ydpi="11.166932" />
<path
sodipodi:type="star"
style="fill:#ffffff;fill-opacity:0.625;stroke:none;stroke-width:4.66924477;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path3159"
sodipodi:sides="3"
sodipodi:cx="11.428571"
sodipodi:cy="175.21933"
sodipodi:r1="168.57143"
sodipodi:r2="84.285713"
sodipodi:arg1="1.5707963"
sodipodi:arg2="2.6179939"
inkscape:flatsided="true"
inkscape:rounded="0.11"
inkscape:randomized="0"
d="M 11.428575,343.79076 C -20.688595,343.79076 -150.61715,118.74791 -134.55857,90.933621 C -118.49998,63.119335 141.35712,63.119328 157.41571,90.933613 C 173.47429,118.7479 43.545746,343.79076 11.428575,343.79076 z"
transform="matrix(0.6555978,-0.5514294,0.5514294,0.6555978,380.12917,420.94953)"
inkscape:export-filename="/home/kilian/workspace/trimage/trimage-icon.png"
inkscape:export-xdpi="11.166932"
inkscape:export-ydpi="11.166932" />
<path
transform="matrix(1.385337,-1.1652199,1.1652199,1.385337,160.77379,177.08667)"
d="M 11.428575,343.79076 C -20.688595,343.79076 -150.61715,118.74791 -134.55857,90.933621 C -118.49998,63.119335 141.35712,63.119328 157.41571,90.933613 C 173.47429,118.7479 43.545746,343.79076 11.428575,343.79076 z"
inkscape:randomized="0"
inkscape:rounded="0.11"
inkscape:flatsided="true"
sodipodi:arg2="2.6179939"
sodipodi:arg1="1.5707963"
sodipodi:r2="84.285713"
sodipodi:r1="168.57143"
sodipodi:cy="175.21933"
sodipodi:cx="11.428571"
sodipodi:sides="3"
id="path3185"
style="fill:url(#radialGradient3187);fill-opacity:1;stroke:none;stroke-width:2.20967627;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:type="star"
inkscape:export-filename="/home/kilian/workspace/trimage/trimage-icon.png"
inkscape:export-xdpi="11.166932"
inkscape:export-ydpi="11.166932" />
<path
sodipodi:type="star"
style="fill:url(#radialGradient3199);fill-opacity:1;stroke:#454545;stroke-width:8.28628635;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path3197"
sodipodi:sides="3"
sodipodi:cx="11.428571"
sodipodi:cy="175.21933"
sodipodi:r1="168.57143"
sodipodi:r2="84.285713"
sodipodi:arg1="1.5707963"
sodipodi:arg2="2.6179939"
inkscape:flatsided="true"
inkscape:rounded="0.11"
inkscape:randomized="0"
d="M 11.428575,343.79076 C -20.688595,343.79076 -150.61715,118.74791 -134.55857,90.933621 C -118.49998,63.119335 141.35712,63.119328 157.41571,90.933613 C 173.47429,118.7479 43.545746,343.79076 11.428575,343.79076 z"
transform="matrix(1.385337,-1.1652199,1.1652199,1.385337,160.77379,177.08667)"
inkscape:export-filename="/home/kilian/workspace/trimage/trimage-icon.png"
inkscape:export-xdpi="11.166932"
inkscape:export-ydpi="11.166932" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

View file

@ -1,6 +1,8 @@
[Desktop Entry]
Name=Trimage image compressor
Comment=A cross-platform tool for optimizing PNG and JPG files.
Name[ru]=Программа для уменьшения веса картинок Trimage
Comment=A cross-platform tool for optimizing PNG and JPG files
Comment[ru]=Кросс-платформенная утилита для оптимизации файлов PNG и JPG
Terminal=false
Icon=trimage
Type=Application

View file

@ -24,7 +24,7 @@ setup(name = "trimage",
('share/applications', ['desktop/trimage.desktop']),
('share/man/man1', ['doc/trimage.1'])],
scripts = ["trimage"],
long_description = """Trimage is a cross-platform GUI and command-line interface to optimize image files via optipng, advpng and jpegoptim, depending on the filetype (currently, PNG and JPG files are supported). It was inspired by imageoptim. All image files are losslessy compressed on the highest available compression levels. Trimage gives you various input functions to fit your own workflow: A regular file dialog, dragging and dropping and various command line options.""",
long_description = """Trimage is a cross-platform GUI and command-line interface to optimize image files via optipng, advpng and guetzli, depending on the filetype (currently, PNG and JPG files are supported). It was inspired by imageoptim. All image files are losslessy compressed on the highest available compression levels. Trimage gives you various input functions to fit your own workflow: A regular file dialog, dragging and dropping and various command line options.""",
requires = ["PyQt4 (>=4.4)"],
#for py2exe

View file

@ -22,7 +22,8 @@ from multiprocessing import cpu_count
from ui import Ui_trimage
VERSION = "1.0.5"
# change version to 1.1 after replacing jpegoptim with guetzli
VERSION = "1.1"
class StartQT4(QMainWindow):
@ -86,7 +87,7 @@ class StartQT4(QMainWindow):
"""Set up the command line options."""
parser = OptionParser(version="%prog " + VERSION,
description="GUI front-end to compress png and jpg images via "
"optipng, advpng and jpegoptim")
"optipng, advpng and guetzli")
parser.set_defaults(verbose=True)
parser.add_option("-v", "--verbose", action="store_true",
@ -261,10 +262,12 @@ class StartQT4(QMainWindow):
"""Check if the required command line apps exist."""
exe = ".exe" if (sys.platform == "win32") else ""
status = False
retcode = self.safe_call("jpegoptim" + exe + " --version")
if retcode != 0:
status = True
sys.stderr.write("[error] please install jpegoptim")
# guetzli does the compression job much better than jpegoptim (and much slower)
#retcode = self.safe_call("jpegoptim" + exe + " --version")
#if retcode != 0:
#status = True
#sys.stderr.write("[error] please install jpegoptim")
retcode = self.safe_call("optipng" + exe + " -v")
if retcode != 0:
@ -282,6 +285,13 @@ class StartQT4(QMainWindow):
sys.stderr.write("[error] please install pngcrush")
return status
# guetzli currently does not have neither --version not --help flags
retcode = self.safe_call("guetzli" + exe)
if retcode != 0:
status = True
sys.stderr.write("[error] please install guetzli")
return status
def safe_call(self, command):
""" cross-platform command-line check """
while True:
@ -431,7 +441,10 @@ class Image:
self.compressing = True
exe = ".exe" if (sys.platform == "win32") else ""
runString = {
"jpeg": u"jpegoptim" + exe + " -f --strip-all '%(file)s'",
# "jpeg": u"jpegoptim" + exe + " -f --strip-all '%(file)s'",
# I commented out jpegoptim because guetzli compresses MUCH BETTER having a similar output quality
# the default quality level for guetzli, if the parameter '--quality Q' is not passed, it 95.
"jpeg": u"guetzli" + exe + " --quality 95 '%(file)s' '%(file)s.bak' && mv '%(file)s.bak' '%(file)s'",
"png": u"optipng" + exe + " -force -o7 '%(file)s'&&advpng" + exe + " -z4 '%(file)s' && pngcrush -rem gAMA -rem alla -rem cHRM -rem iCCP -rem sRGB -rem time '%(file)s' '%(file)s.bak' && mv '%(file)s.bak' '%(file)s'"
}
# Create a backup file

0
trimage Normal file → Executable file
View file