Compare commits
183 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad74684272 | ||
|
|
031adf6054 | ||
|
|
f35be9c750 | ||
|
|
636b5a9750 | ||
|
|
c21089f97b | ||
|
|
f37c8b6ffc | ||
|
|
8af532f25e | ||
|
|
493a0e18d2 | ||
|
|
224f6b3503 | ||
|
|
c3244e19a6 | ||
|
|
269e052ba4 | ||
|
|
49272e0d95 | ||
|
|
1342503b54 | ||
|
|
0cbbfcdfe8 | ||
|
|
9d6edd3847 | ||
|
|
9bdd44a4e4 | ||
|
|
8376c4a238 | ||
|
|
54d4b74172 | ||
|
|
e3b5146977 | ||
|
|
f2ff0448c4 | ||
|
|
5f5757f575 | ||
|
|
a3f417c43e | ||
|
|
d6118b520b | ||
|
|
f5ff57ff48 | ||
|
|
d8d207438e | ||
|
|
27b22807bc | ||
|
|
891bc9b73f | ||
|
|
5553e7fabe | ||
|
|
9ea961e87c | ||
|
|
c040b63595 | ||
|
|
b829b6ac5f | ||
|
|
be7fe35ba9 | ||
|
|
915a360a09 | ||
|
|
06aad62cb6 | ||
|
|
fa94b546fb | ||
|
|
1ecff8b3d9 | ||
|
|
9ee9548d47 | ||
|
|
4e702d3f3a | ||
|
|
006256e08b | ||
|
|
117a1a36cc | ||
|
|
1e602b2a77 | ||
|
|
460161b6ee | ||
|
|
030684fe60 | ||
|
|
b2713d5485 | ||
|
|
95512644b6 | ||
|
|
fb1c463a03 | ||
|
|
d2c0a7905b | ||
|
|
13f43bfa2f | ||
|
|
f7531a1095 | ||
|
|
02a92463e5 | ||
|
|
c38cf92f25 | ||
|
|
26878908c0 | ||
|
|
42008757f2 | ||
|
|
8e415cb451 | ||
|
|
23c3243209 | ||
|
|
3a19f3cef8 | ||
|
|
59b3d8c94d | ||
|
|
573dcab3d2 | ||
|
|
d7a0f84f64 | ||
|
|
797cc433b3 | ||
|
|
461ae9fd57 | ||
|
|
648f78c139 | ||
|
|
352ba03d02 | ||
|
|
278ef269cf | ||
|
|
25836634af | ||
|
|
a24ef15ff5 | ||
|
|
063d2b8adb | ||
|
|
420c945f94 | ||
|
|
bc8c665d40 | ||
|
|
e71c22b519 | ||
|
|
69fb5c9c88 | ||
|
|
623922d12d | ||
|
|
a20f7a5beb | ||
|
|
a3d9735ef8 | ||
|
|
cd80b8c386 | ||
|
|
8eca530275 | ||
|
|
e47888e067 | ||
|
|
8503c851b3 | ||
|
|
3b99c65a8c | ||
|
|
a3bc8da5e6 | ||
|
|
8a8ffa66ef | ||
|
|
f17cbf980a | ||
|
|
44171bc991 | ||
|
|
7bee242cf4 | ||
|
|
20a4c4e4ec | ||
|
|
8227aee846 | ||
|
|
6ca6082e19 | ||
|
|
8c74e8b0df | ||
|
|
b9a10eed29 | ||
|
|
07f5dd0c83 | ||
|
|
e460faca76 | ||
|
|
4c034722c7 | ||
|
|
83ea31ed22 | ||
|
|
b27fec1a02 | ||
|
|
fbb92b153d | ||
|
|
bf26c6bb43 | ||
|
|
9d25bc738b | ||
|
|
cdf9bca4fa | ||
|
|
966536bb9a | ||
|
|
671f4163a8 | ||
|
|
8a719be207 | ||
|
|
f181fdcd00 | ||
|
|
646abfca85 | ||
|
|
bb9754a24e | ||
|
|
d2a1c1e7d0 | ||
|
|
8715233259 | ||
|
|
454d5e6d9f | ||
|
|
34e4f6581f | ||
|
|
83d5aebe66 | ||
|
|
ee102f2289 | ||
|
|
c243524eaa | ||
|
|
e4a7a1a254 | ||
|
|
2596905266 | ||
|
|
de35d5967e | ||
|
|
ea4350661a | ||
|
|
52ccd5a1de | ||
|
|
c9ae552994 | ||
|
|
cf37894afc | ||
|
|
8e8cac3390 | ||
|
|
2c2ac98b5d | ||
|
|
407806f187 | ||
|
|
54680e1bc6 | ||
|
|
4c5caf0d9c | ||
|
|
f7988ab5bd | ||
|
|
d03c0dfbd8 | ||
|
|
c6cb2dc321 | ||
|
|
1be7cc83af | ||
|
|
45410e7b4b | ||
|
|
8a89a44c9e | ||
|
|
7e8a0b7cd6 | ||
|
|
a0edd4f003 | ||
|
|
ba0bb856d2 | ||
|
|
773efc6870 | ||
|
|
7cf707d5a3 | ||
|
|
a936903b00 | ||
|
|
d282bcf8e9 | ||
|
|
36644c5bf4 | ||
|
|
464337d033 | ||
|
|
cdd11c92ee | ||
|
|
348f97fabc | ||
|
|
8e781f5409 | ||
|
|
e77f6e5a59 | ||
|
|
451269d960 | ||
|
|
1106ca482c | ||
|
|
0f5092e10b | ||
|
|
29be211a9d | ||
|
|
51691351d3 | ||
|
|
cf9d98027c | ||
|
|
97956d7e14 | ||
|
|
5782698dda | ||
|
|
a5da8a9a58 | ||
|
|
2c9599813a | ||
|
|
305198e09e | ||
|
|
c9126ae196 | ||
|
|
a3a31a7e93 | ||
|
|
ecfc4af6a3 | ||
|
|
b0b0dd9244 | ||
|
|
9fafa54f2e | ||
|
|
a89a8a3302 | ||
|
|
022df47501 | ||
|
|
cbd4d76b6e | ||
|
|
591536fa9e | ||
|
|
b26ff08e96 | ||
|
|
1862f6f019 | ||
|
|
a506c0fee4 | ||
|
|
e382b579ec | ||
|
|
19308292c0 | ||
|
|
70181f97ef | ||
|
|
8dcec263ff | ||
|
|
41672b8a39 | ||
|
|
c797699ff7 | ||
|
|
a24d69a178 | ||
|
|
7efecb7b89 | ||
|
|
3f26595198 | ||
|
|
d79a5c498b | ||
|
|
6a00b2e25c | ||
|
|
e96327edbd | ||
|
|
27d3fd782c | ||
|
|
9826e6cd97 | ||
|
|
bf763ec335 | ||
|
|
c0dceb88ad | ||
|
|
b86c5aa8cd | ||
|
|
a283ce8481 |
116
.gitignore
vendored
|
|
@ -1,2 +1,116 @@
|
|||
*.pyc
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
|
|
|||
3
MANIFEST.in
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
include COPYING MANIFEST MANIFEST.in README.md bin/trimage
|
||||
recursive-include desktop *.svg *.desktop
|
||||
recursive-include trimage/ *.py *.png
|
||||
44
README.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
### Made by [@kilianvalkhof](https://twitter.com/kilianvalkhof)
|
||||
|
||||
#### Other projects:
|
||||
|
||||
- 💻 [Polypane](https://polypane.app) - Develop responsive websites and apps twice as fast on multiple screens at once
|
||||
- 🖌️ [Superposition](https://superposition.design) - Kickstart your design system by extracting design tokens from your website
|
||||
- 🗒️ [FromScratch](https://fromscratch.rocks) - A smart but simple autosaving scratchpad
|
||||
|
||||
---
|
||||
|
||||
# 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 [advpng](http://advancemame.sourceforge.net/comp-readme.html), [jpegoptim](http://www.kokkonen.net/tjko/projects.html), [optipng](http://optipng.sourceforge.net) and [pngcrush](https://pmt.sourceforge.io/pngcrush) 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.
|
||||
|
||||
## Installation instructions
|
||||
|
||||
Visit [Trimage.org](http://trimage.org) to install Trimage as a package.
|
||||
|
||||
## Building instructions
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- PyQt5
|
||||
- advpng
|
||||
- jpegoptim
|
||||
- optipng
|
||||
- pngcrush
|
||||
|
||||
### Build from source
|
||||
|
||||
Build and install by running:
|
||||
|
||||
python setup.py build
|
||||
sudo python setup.py install
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
h2. 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 "jpegoptim":http://www.kokkonen.net/tjko/projects.html, depending on the filetype (currently, PNG and JPG files are supported). It was inspired by "imageoptim":http://imageoptim.pornel.net/. 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.
|
||||
|
||||
Visit "Trimage.org":http://trimage.org for more information
|
||||
|
||||
17
TODO.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Todo
|
||||
|
||||
- general refactoring
|
||||
- sys.exit(1) for errors -- how to handle? Not good to simply sys.exit() from any random part of code (can leave things in a mess)
|
||||
- consider context managers for handling compression, so as to keep operations atomic and/or rollback-able
|
||||
- add a recursive option on the command-line for use with -d
|
||||
- make -f accept a list of files
|
||||
- make the current verbose be "normal", and make -verbose print the commandline app prints as well
|
||||
- find a way to specify the version once for everywhere
|
||||
- notification area drag/drop widget -> probably need gtk for gnome
|
||||
- figure out how to make mac and win versions (someone else :) <- via gui2exe
|
||||
- animate compressing.gif
|
||||
- allow selection/deletion of rows from table (and subsequently the imagelist)
|
||||
- punypng api? http://www.gracepointafterfive.com/punypng/api
|
||||
- imagemagick/graphicsmagick?
|
||||
- always on top option
|
||||
- intelligently recompress, i.e. go through the list of files, recompress each until no more gains are seen (and a sensible number-of-tries limit isn't exceeded), and flag that file as fully-optimised. Repeat for each file in the list, until all are done. Saves pointlessly trying to optimise files. Consider the case of a directory of 100 files, already optimised once. Recompressing maximally compresses 90. Recompressing again would currently try to recompress all 100, when only 10 would be worthy of trying to compress further
|
||||
23
bin/trimage
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
#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:])
|
||||
80
debian/changelog
vendored
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
trimage (1.0.6-0ubuntu1) bionic; urgency=low
|
||||
|
||||
* Migrate to python 3 and python-qt 5
|
||||
* fix bug with hanging thread
|
||||
* remove hurry.filesize package
|
||||
|
||||
-- Kilian Valkhof <kilian@kilianvalkhof.com> Tue, 12 Mar 2019 11:54:00 +0200
|
||||
|
||||
trimage (1.0.5-0ubuntu1) jaunty; urgency=low
|
||||
|
||||
* prevent images from becoming larger after recompression
|
||||
* prevent images from being written when not writeable
|
||||
* allow dragging of entire directories
|
||||
* performance increases for large numbers of files
|
||||
|
||||
-- Kilian Valkhof <kilian@kilianvalkhof.com> Sun, 26 Sep 2010 15:24:04 +0200
|
||||
|
||||
trimage (1.0.4-0ubuntu1) jaunty; urgency=low
|
||||
|
||||
* prevent systemtray from starting if a CLI compression is done
|
||||
* preliminary OSX work
|
||||
|
||||
-- Kilian Valkhof <kilian@kilianvalkhof.com> Fri, 17 Sep 2010 20:45:16 +0200
|
||||
|
||||
trimage (1.0.3-0ubuntu1) jaunty; urgency=low
|
||||
|
||||
* fix for environments without supported system tray
|
||||
* add copyright info for filesize.py
|
||||
* update various debian configurations
|
||||
|
||||
-- Kilian Valkhof <kilian@kilianvalkhof.com> Sat, 12 Jun 2010 14:04:35 +0200
|
||||
|
||||
trimage (1.0.2-0ubuntu1) jaunty; urgency=low
|
||||
|
||||
* save window geometry and file directory upon closing
|
||||
* prettier systray menu
|
||||
* Switch to dpkg-source 3.0(quilt) format
|
||||
|
||||
-- Kilian Valkhof <kilian@kilianvalkhof.com> Thu, 03 Jun 2010 13:01:30 +0200
|
||||
|
||||
trimage (1.0.1b2-0ubuntu1) jaunty; urgency=low
|
||||
|
||||
* use a threadpool for images
|
||||
* more robust file handling
|
||||
* re-adding images now results in recompressing them
|
||||
* compressing message now shows filename
|
||||
* wider array of status messages in the table
|
||||
* add a notification area icon
|
||||
* remove BOM
|
||||
-- Kilian Valkhof <kilian@kdesktop> Mon, 29 Mar 2010 13:08:18 +0200
|
||||
|
||||
trimage (1.0.1b-0ubuntu1) jaunty; urgency=low
|
||||
|
||||
* use a threadpool for images
|
||||
* more robust file handling
|
||||
* re-adding images now results in recompressing them
|
||||
* compressing message now shows filename
|
||||
* wider array of status messages in the table
|
||||
* add a notification area icon
|
||||
-- Kilian Valkhof <kilian@kdesktop> Mon, 29 Mar 2010 13:08:18 +0200
|
||||
|
||||
trimage (1.0.0b3-0ubuntu1) jaunty; urgency=low
|
||||
|
||||
* better image filetype determination
|
||||
* use unicode strings everywhere
|
||||
* fix bug where Trimage would crash if a directory didn't end in a slash
|
||||
* avoid a segfault when parsing empty directories
|
||||
* ../../debian/changelog
|
||||
-- Kilian Valkhof <kilian@kdesktop> Mon, 29 Mar 2010 13:08:18 +0200
|
||||
|
||||
trimage (1.0.0b2-0ubuntu1) jaunty; urgency=low
|
||||
|
||||
* correct parsing of unicode characters in filenames
|
||||
* changelog
|
||||
-- Kilian Valkhof <kilian@kdesktop> Sun, 28 Mar 2010 16:09:45 +0200
|
||||
|
||||
trimage (1.0.0b-0ubuntu1) jaunty; urgency=low
|
||||
|
||||
* Trimage image compressor
|
||||
-- Kilian Valkhof <help@trimage.org> Tue, 23 Mar 2010 20:18:17 +0100
|
||||
1
debian/compat
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
11
|
||||
18
debian/control
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
Source: trimage
|
||||
Section: graphics
|
||||
Priority: optional
|
||||
Maintainer: Kilian Valkhof <kilian@kilianvalkhof.com>
|
||||
Build-Depends: debhelper (>=7), python3
|
||||
Standards-Version: 4.3.0
|
||||
Homepage: http://trimage.org
|
||||
|
||||
Package: trimage
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends}, ${python:Depends}, python-pyqt5 (>=5.7), optipng (>=0.6.2.1), advancecomp (>=1.15), jpegoptim (>=1.2.2), pngcrush (>=1.6.7)
|
||||
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
|
||||
(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.
|
||||
212
debian/copyright
vendored
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
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.
|
||||
|
||||
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.
|
||||
1
debian/docs
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
README
|
||||
3
debian/rules
vendored
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/make -f
|
||||
%:
|
||||
dh $@
|
||||
2
debian/source/format
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
3.0 (quilt)
|
||||
|
||||
10
desktop/trimage.desktop
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
[Desktop Entry]
|
||||
Name=Trimage image compressor
|
||||
Comment=A cross-platform tool for optimizing PNG and JPG files.
|
||||
Terminal=false
|
||||
Icon=trimage
|
||||
Type=Application
|
||||
Exec=trimage
|
||||
Categories=Application;Qt;Graphics;
|
||||
StartupNotify=true
|
||||
Keywords=compression;compressor;images;jpg;jpeg;png;web;
|
||||
|
|
@ -9,13 +9,14 @@
|
|||
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="744.09448819"
|
||||
height="1052.3622047"
|
||||
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">
|
||||
inkscape:output_extension="org.inkscape.output.svg.inkscape"
|
||||
version="1.0">
|
||||
<defs
|
||||
id="defs4">
|
||||
<linearGradient
|
||||
|
|
@ -113,7 +114,7 @@
|
|||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.98994949"
|
||||
inkscape:cx="299.62219"
|
||||
inkscape:cy="722.03051"
|
||||
inkscape:cy="479.5939"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
|
|
@ -135,10 +136,11 @@
|
|||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
id="layer1"
|
||||
transform="translate(-72.611421,-111.20599)">
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:url(#linearGradient3183);fill-opacity:1;stroke:none;stroke-width:2.20967632;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
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"
|
||||
|
|
@ -157,7 +159,7 @@
|
|||
inkscape:export-ydpi="11.166932" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#ffffff;fill-opacity:0.45833333999999998;stroke:none;stroke-width:3.01696083;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
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"
|
||||
|
|
@ -176,7 +178,7 @@
|
|||
inkscape:export-ydpi="11.166932" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#ffffff;fill-opacity:0.62500000000000000;stroke:none;stroke-width:4.66924454;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
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"
|
||||
|
|
@ -207,14 +209,14 @@
|
|||
sodipodi:cx="11.428571"
|
||||
sodipodi:sides="3"
|
||||
id="path3185"
|
||||
style="fill:url(#radialGradient3187);fill-opacity:1;stroke:none;stroke-width:2.20967632;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
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.28628619;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
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"
|
||||
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 486 B |
46
doc/trimage.1
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
.\" Copyright (C) 2011 Kyrill Detinov <lazy.kent@opensuse.org>
|
||||
.\"
|
||||
.\" This manual page is distributed under the terms
|
||||
.\" of the GNU Free Documentation License version 1.3.
|
||||
.\"
|
||||
.TH TRIMAGE "1" "2019-03-12" "trimage 1.0.6" "User Commands"
|
||||
|
||||
.SH NAME
|
||||
trimage \- losslessly optimizing png and jpeg liles
|
||||
|
||||
.SH SYNOPSIS
|
||||
.B trimage
|
||||
.RI [ options ]
|
||||
|
||||
.SH DESCRIPTION
|
||||
Front\-end to compress png and jpeg images via optipng, advpng, pngcrush
|
||||
and jpegoptim.
|
||||
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
\fB\-d\fI directory\fR, \fB\-\-directory\fR=\fIdirectory\fR
|
||||
Compresses images in directory.
|
||||
.TP
|
||||
\fB\-f\fI filename\fR, \fB\-\-file\fR=\fIfilename\fR
|
||||
Compresses image.
|
||||
.TP
|
||||
\fB\-h\fR, \fB\-\-help\fR
|
||||
Show help message.
|
||||
.TP
|
||||
\fB\-q\fR, \fB\-\-quiet\fR
|
||||
Quiet mode.
|
||||
.TP
|
||||
\fB\-v\fR, \fB\-\-verbose\fR
|
||||
Verbose mode (default).
|
||||
.TP
|
||||
\fB\-\-version\fR
|
||||
Show program version number.
|
||||
|
||||
.SH "SEE ALSO"
|
||||
.BR advpng (1),
|
||||
.BR jpegoptim (1),
|
||||
.BR opt-png (1),
|
||||
.BR opt-jpg (1),
|
||||
.BR pngcrush (1),
|
||||
.BR pngrecolor (1),
|
||||
.BR pngstrip (1)
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
# this is a namespace package
|
||||
try:
|
||||
import pkg_resources
|
||||
pkg_resources.declare_namespace(__name__)
|
||||
except ImportError:
|
||||
import pkgutil
|
||||
__path__ = pkgutil.extend_path(__path__, __name__)
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
hurry.filesize
|
||||
==============
|
||||
|
||||
hurry.filesize a simple Python library that can take a number of bytes and
|
||||
returns a human-readable string with the size in it, in kilobytes (K),
|
||||
megabytes (M), etc.
|
||||
|
||||
The default system it uses is "traditional", where multipliers of 1024
|
||||
increase the unit size::
|
||||
|
||||
>>> from hurry.filesize import size
|
||||
>>> size(1024)
|
||||
'1K'
|
||||
|
||||
An alternative, slightly more verbose system::
|
||||
|
||||
>>> from hurry.filesize import alternative
|
||||
>>> size(1, system=alternative)
|
||||
'1 byte'
|
||||
>>> size(10, system=alternative)
|
||||
'10 bytes'
|
||||
>>> size(1024, system=alternative)
|
||||
'1 KB'
|
||||
|
||||
A verbose system::
|
||||
|
||||
>>> from hurry.filesize import verbose
|
||||
>>> size(10, system=verbose)
|
||||
'10 bytes'
|
||||
>>> size(1024, system=verbose)
|
||||
'1 kilobyte'
|
||||
>>> size(2000, system=verbose)
|
||||
'1 kilobyte'
|
||||
>>> size(3000, system=verbose)
|
||||
'2 kilobytes'
|
||||
>>> size(1024 * 1024, system=verbose)
|
||||
'1 megabyte'
|
||||
>>> size(1024 * 1024 * 3, system=verbose)
|
||||
'3 megabytes'
|
||||
|
||||
You can also use the SI system, where multipliers of 1000 increase the unit
|
||||
size::
|
||||
|
||||
>>> from hurry.filesize import si
|
||||
>>> size(1000, system=si)
|
||||
'1K'
|
||||
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
from hurry.filesize.filesize import size
|
||||
from hurry.filesize.filesize import traditional, alternative, verbose, iec, si
|
||||
|
||||
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
|
||||
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
|
||||
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import unittest, doctest
|
||||
|
||||
def test_suite():
|
||||
return unittest.TestSuite((
|
||||
doctest.DocFileSuite('README.txt'),
|
||||
doctest.DocTestSuite('hurry.filesize.filesize'),
|
||||
))
|
||||
237
resources/trimage.svg
Normal 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 |
|
|
@ -127,7 +127,7 @@
|
|||
<cursorShape>PointingHandCursor</cursorShape>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Recompress selected images</string>
|
||||
<string>Recompress all images</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Recompress</string>
|
||||
22
setup.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from distutils.core import setup
|
||||
|
||||
|
||||
setup(name = "trimage",
|
||||
version = "1.0.6",
|
||||
description = "Trimage image compressor - A cross-platform tool for optimizing PNG and JPG files",
|
||||
author = "Kilian Valkhof, Paul Chaplin",
|
||||
author_email = "help@trimage.org",
|
||||
url = "http://trimage.org",
|
||||
license = "MIT license",
|
||||
packages = ["trimage",
|
||||
"trimage.ThreadPool"],
|
||||
package_data = {"trimage" : ["pixmaps/*.*"] },
|
||||
data_files=[('share/icons/hicolor/scalable/apps', ['desktop/trimage.svg']),
|
||||
('share/applications', ['desktop/trimage.desktop']),
|
||||
('share/man/man1', ['doc/trimage.1'])],
|
||||
scripts = ["bin/trimage"],
|
||||
long_description = """Trimage is a cross-platform GUI and command-line interface to optimize image files via advpng, jpegoptim, optipng and pngcrush, 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 = ["PyQt5"]
|
||||
)
|
||||
38
todo
|
|
@ -1,38 +0,0 @@
|
|||
==========================================
|
||||
todo app wise
|
||||
- general refactoring
|
||||
- sys.exit(1) for errors -- how to handle? Not good to simply sys.exit() from
|
||||
any random part of code (can leave things in a mess)
|
||||
- consider context managers for handling compression, so as to keep operations
|
||||
atomic and/or rollback-able
|
||||
- add a recursive option on the command-line for use with -d
|
||||
- make -f accept a list of files
|
||||
- make the current verbose be "normal", and make -verbose print the commandline
|
||||
app prints as well
|
||||
- verify that a *recompressed* file is smaller than the compressed one
|
||||
|
||||
todo else
|
||||
- figure out dependencies for a .deb/how to make a .deb <- via launchpad
|
||||
- figure out how to make mac and win versions (someone else :) <- via gui2exe
|
||||
|
||||
todo later
|
||||
- use multiprocessing lib to take advantage of multicore/multi-CPU to compress
|
||||
multiple files simultaneously (threads have issues in Python; see "GIL")
|
||||
|
||||
===========================================
|
||||
later versions:
|
||||
animate compressing.gif
|
||||
allow selection/deletion of rows from table (and subsequently the imagelist)
|
||||
check for double files when adding
|
||||
punypng api? http://www.gracepointafterfive.com/punypng/api
|
||||
imagemagick/graphicsmagick?
|
||||
always on top option
|
||||
notification area widget
|
||||
intelligently recompress, i.e. go through the list of files, recompress
|
||||
each until no more gains are seen (and a sensible number-of-tries limit
|
||||
isn't exceeded), and flag that file as fully-optimised. Repeat for each
|
||||
file in the list, until all are done. Saves pointlessly trying to
|
||||
optimise files. Consider the case of a directory of 100 files, already
|
||||
optimised once. Recompressing maximally compresses 90. Recompressing
|
||||
again would currently try to recompress all 100, when only 10 would be
|
||||
worthy of trying to compress further.
|
||||
BIN
trimage-icon.png
|
Before Width: | Height: | Size: 4.4 KiB |
347
trimage.py
|
|
@ -1,347 +0,0 @@
|
|||
import sys
|
||||
from os import listdir
|
||||
from os import path
|
||||
from subprocess import call, PIPE
|
||||
from optparse import OptionParser
|
||||
|
||||
from PyQt4.QtCore import *
|
||||
from PyQt4.QtGui import *
|
||||
from hurry.filesize import *
|
||||
|
||||
from ui import Ui_trimage
|
||||
|
||||
VERSION = "1.0.0"
|
||||
|
||||
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 = []
|
||||
|
||||
# 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)
|
||||
|
||||
# activate command line options
|
||||
self.commandline_options()
|
||||
|
||||
def commandline_options(self):
|
||||
"""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")
|
||||
|
||||
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()
|
||||
|
||||
# send to correct function
|
||||
if options.filename:
|
||||
self.file_from_cmd(options.filename)
|
||||
if options.directory:
|
||||
self.dir_from_cmd(options.directory)
|
||||
|
||||
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(path.dirname(directory))
|
||||
imagedir = listdir(directory)
|
||||
filelist = QStringList()
|
||||
for image in imagedir:
|
||||
image = QString(path.join(dirpath, image))
|
||||
filelist.append(image)
|
||||
self.delegator(filelist)
|
||||
|
||||
def file_from_cmd(self, image):
|
||||
"""Get the file and send it to compress_file"""
|
||||
self.showapp = False
|
||||
image = path.abspath(image)
|
||||
filecmdlist = QStringList()
|
||||
filecmdlist.append(image)
|
||||
self.delegator(filecmdlist)
|
||||
|
||||
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)
|
||||
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.delegator(images)
|
||||
|
||||
def recompress_files(self):
|
||||
"""Send each file in the current file list to compress_file again."""
|
||||
newimagelist = []
|
||||
for image in self.imagelist:
|
||||
newimagelist.append(image[4])
|
||||
self.imagelist = []
|
||||
self.delegator(newimagelist)
|
||||
|
||||
"""
|
||||
Compress functions
|
||||
"""
|
||||
|
||||
def delegator(self, images):
|
||||
"""
|
||||
Recieve all images, check them and send them to the worker thread.
|
||||
"""
|
||||
delegatorlist = []
|
||||
for image in images:
|
||||
if self.checkname(image):
|
||||
delegatorlist.append((image, QIcon(image)))
|
||||
self.imagelist.append(("Compressing...", "", "", "", image,
|
||||
QIcon(QPixmap("compressing.gif"))))
|
||||
else:
|
||||
sys.stderr.write("[error] %s not an image file" % image)
|
||||
|
||||
self.update_table()
|
||||
self.thread.compress_file(delegatorlist, self.showapp, self.verbose,
|
||||
self.imagelist)
|
||||
|
||||
|
||||
"""
|
||||
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 checkname(self, name):
|
||||
"""Check if the file is a jpg or png."""
|
||||
return path.splitext(str(name))[1].lower() in [".jpg", ".jpeg", ".png"]
|
||||
|
||||
def enable_recompress(self):
|
||||
"""Enable the recompress button."""
|
||||
self.ui.recompress.setEnabled(True)
|
||||
|
||||
def checkapps(self):
|
||||
"""Check if the required command line apps exist."""
|
||||
status = False
|
||||
retcode = call("jpegoptim --version", shell=True, stdout=PIPE)
|
||||
if retcode != 0:
|
||||
status = True
|
||||
sys.stderr.write("[error] please install jpegoptim")
|
||||
|
||||
retcode = call("optipng -v", shell=True, stdout=PIPE)
|
||||
if retcode != 0:
|
||||
status = True
|
||||
sys.stderr.write("[error] please install optipng")
|
||||
|
||||
retcode = call("advpng --version", shell=True, stdout=PIPE)
|
||||
if retcode != 0:
|
||||
status = True
|
||||
sys.stderr.write("[error] please install advancecomp")
|
||||
return status
|
||||
|
||||
|
||||
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()][5]
|
||||
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 Worker(QThread):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QThread.__init__(self, parent)
|
||||
self.exiting = False
|
||||
|
||||
def __del__(self):
|
||||
self.exiting = True
|
||||
self.wait()
|
||||
|
||||
def compress_file(self, images, showapp, verbose, imagelist):
|
||||
"""Start the worker thread."""
|
||||
self.images = images
|
||||
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."""
|
||||
for image in self.images:
|
||||
#gather old file data
|
||||
filename = str(image[0])
|
||||
icon = image[1]
|
||||
oldfile = QFileInfo(filename)
|
||||
name = oldfile.fileName()
|
||||
oldfilesize = oldfile.size()
|
||||
oldfilesizestr = size(oldfilesize, system=alternative)
|
||||
|
||||
# get extention
|
||||
extention = path.splitext(filename)[1]
|
||||
#decide with tool to use
|
||||
if extention in [".jpg", ".jpeg"]:
|
||||
runString = "jpegoptim -f --strip-all '%(file)s'"
|
||||
elif extention in [".png"]:
|
||||
runString = ("optipng -force -o7 '%(file)s';"
|
||||
"advpng -z4 '%(file)s'")
|
||||
else:
|
||||
sys.stderr.write("[error] %s not an image file" % filename)
|
||||
|
||||
try:
|
||||
retcode = call(runString % {"file": filename}, shell=True,
|
||||
stdout=PIPE)
|
||||
runfile = retcode
|
||||
except OSError as e:
|
||||
runfile = e
|
||||
|
||||
if runfile == 0:
|
||||
#gather new file data
|
||||
newfile = QFile(filename)
|
||||
newfilesize = newfile.size()
|
||||
newfilesizestr = size(newfilesize, system=alternative)
|
||||
|
||||
#calculate ratio and make a nice string
|
||||
ratio = 100 - (float(newfilesize) / float(oldfilesize) * 100)
|
||||
ratiostr = "%.1f%%" % ratio
|
||||
|
||||
# append current image to list
|
||||
for i, image in enumerate(self.imagelist):
|
||||
if image[4] == filename:
|
||||
self.imagelist.remove(image)
|
||||
self.imagelist.insert(i, (name, oldfilesizestr,
|
||||
newfilesizestr, ratiostr, filename, icon))
|
||||
|
||||
self.emit(SIGNAL("updateUi"))
|
||||
|
||||
if not self.showapp and self.verbose:
|
||||
# we work via the commandline
|
||||
print("File: " + filename + ", Old Size: "
|
||||
+ oldfilesizestr + ", New Size: " + newfilesizestr
|
||||
+ ", Ratio: " + ratiostr)
|
||||
else:
|
||||
sys.stderr.write("[error] %s" % runfile)
|
||||
|
||||
if not self.showapp:
|
||||
#make sure the app quits after all images are done
|
||||
quit()
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
myapp = StartQT4()
|
||||
|
||||
if myapp.showapp:
|
||||
myapp.show()
|
||||
sys.exit(app.exec_())
|
||||
275
trimage/ThreadPool/ThreadPool.py
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
'''
|
||||
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.__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])
|
||||
|
||||
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 as 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 as _: #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
|
||||
1
trimage/ThreadPool/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from .ThreadPool import ThreadPool, ThreadPoolMixIn
|
||||
0
trimage/__init__.py
Normal file
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 676 B After Width: | Height: | Size: 676 B |
BIN
trimage/pixmaps/trimage-icon.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 2 KiB |
45
trimage/tools.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import errno
|
||||
from subprocess import call, PIPE
|
||||
|
||||
|
||||
def check_dependencies():
|
||||
"""Check if the required command line apps exist."""
|
||||
status = True
|
||||
dependencies = {
|
||||
"jpegoptim": "--version",
|
||||
"optipng": "-v",
|
||||
"advpng": "--version",
|
||||
"pngcrush": "-version"
|
||||
}
|
||||
|
||||
for elt in dependencies:
|
||||
retcode = safe_call(elt + " " + dependencies[elt])
|
||||
if retcode != 0:
|
||||
status = False
|
||||
print("[error] please install {}".format(elt), file=sys.stderr)
|
||||
|
||||
return status
|
||||
|
||||
|
||||
def safe_call(command):
|
||||
"""Cross-platform command-line check."""
|
||||
while True:
|
||||
try:
|
||||
return call(command, shell=True, stdout=PIPE)
|
||||
except OSError as e:
|
||||
if e.errno == errno.EINTR:
|
||||
continue
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
def human_readable_size(num, suffix="B"):
|
||||
"""Bytes to a readable size format"""
|
||||
for unit in ["", "K", "M", "G", "T", "P", "E", "Z"]:
|
||||
if abs(num) < 1024.0:
|
||||
return "%3.1f%s%s" % (num, unit, suffix)
|
||||
num /= 1024.0
|
||||
return "%.1f%s%s" % (num, "Y", suffix)
|
||||
497
trimage/trimage.py
Normal file
|
|
@ -0,0 +1,497 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import time
|
||||
import sys
|
||||
from os import listdir, path, remove, access, W_OK
|
||||
from shutil import copy
|
||||
|
||||
from optparse import OptionParser
|
||||
from multiprocessing import cpu_count
|
||||
from queue import Queue
|
||||
|
||||
from PyQt5.QtCore import *
|
||||
from PyQt5.QtGui import *
|
||||
from PyQt5.QtWidgets import *
|
||||
|
||||
from ThreadPool import ThreadPool
|
||||
from ui import Ui_trimage
|
||||
from tools import *
|
||||
|
||||
|
||||
VERSION = "1.0.6"
|
||||
|
||||
|
||||
class StartQt(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()
|
||||
if self.settings.value("geometry"):
|
||||
self.restoreGeometry(self.settings.value("geometry"))
|
||||
|
||||
# check if dependencies are installed
|
||||
if not check_dependencies():
|
||||
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)
|
||||
|
||||
# make a worker thread
|
||||
self.thread = Worker()
|
||||
|
||||
# connect signals with slots
|
||||
self.ui.addfiles.clicked.connect(self.file_dialog)
|
||||
self.ui.recompress.clicked.connect(self.recompress_files)
|
||||
self.quit_shortcut.activated.connect(self.close)
|
||||
self.ui.processedfiles.drop_event_signal.connect(self.file_drop)
|
||||
self.thread.finished.connect(self.update_table)
|
||||
self.thread.update_ui_signal.connect(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):
|
||||
"""Set up the command line options."""
|
||||
self.cli = False
|
||||
parser = OptionParser(version="%prog " + VERSION,
|
||||
description="GUI front-end to compress png and jpg images via "
|
||||
"advpng, jpegoptim, optipng and pngcrush")
|
||||
|
||||
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:
|
||||
self.thread.finished.connect(quit)
|
||||
self.cli = True
|
||||
|
||||
# send to correct function
|
||||
if options.filename:
|
||||
self.file_from_cmd(options.filename)
|
||||
if options.directory:
|
||||
self.dir_from_cmd(options.directory)
|
||||
|
||||
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)
|
||||
if (self.settings.value("fdstate")):
|
||||
fd.restoreState(self.settings.value("fdstate"))
|
||||
directory = self.settings.value("directory", QVariant(""))
|
||||
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(images[0])))
|
||||
self.delegator([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 = next(i.image for i in self.imagelist
|
||||
if i.image.fullpath == fullpath)
|
||||
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("[error] {} not a supported image file and/or not writable".format(image.fullpath), file=sys.stderr)
|
||||
|
||||
"""
|
||||
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()
|
||||
|
||||
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 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 = {
|
||||
'filename_w_ext': lambda i: self.statusStr().format(i.filename_w_ext),
|
||||
'oldfilesizestr': lambda i: human_readable_size(i.oldfilesize)
|
||||
if i.compressed else "",
|
||||
'newfilesizestr': lambda i: human_readable_size(i.newfilesize)
|
||||
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 = ['filename_w_ext', '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: {0}"
|
||||
if self.image.compressing:
|
||||
message = "Compressing {0}..."
|
||||
return message
|
||||
if not self.image.compressed and self.image.recompression:
|
||||
return "Queued for recompression {0}..."
|
||||
if not self.image.compressed:
|
||||
return "Queued {0}..."
|
||||
return "{0}"
|
||||
|
||||
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
|
||||
self.filename_w_ext = path.basename(self.fullpath)
|
||||
self.filename, self.filetype = path.splitext(self.filename_w_ext)
|
||||
if path.isfile(self.fullpath) and access(self.fullpath, W_OK):
|
||||
self.filetype = self.filetype[1:].lower()
|
||||
if self.filetype == "jpg":
|
||||
self.filetype = "jpeg"
|
||||
if self.filetype in ["jpeg", "png"]:
|
||||
oldfile = QFileInfo(self.fullpath)
|
||||
self.oldfilesize = oldfile.size()
|
||||
self.icon = QIcon(self.fullpath)
|
||||
self.valid = True
|
||||
|
||||
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
|
||||
runString = {
|
||||
"jpeg": "jpegoptim -f --strip-all '%(file)s'",
|
||||
"png": "optipng -force -o7 '%(file)s'&&advpng -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
|
||||
backupfullpath = '/tmp/' + self.filename_w_ext
|
||||
copy(self.fullpath, backupfullpath)
|
||||
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(backupfullpath, self.fullpath)
|
||||
self.newfilesize = self.oldfilesize
|
||||
|
||||
# removes the backup file
|
||||
remove(backupfullpath)
|
||||
else:
|
||||
self.failed = True
|
||||
self.compressing = False
|
||||
self.retcode = retcode
|
||||
return self
|
||||
|
||||
|
||||
class Worker(QThread):
|
||||
update_ui_signal = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QThread.__init__(self, parent)
|
||||
self.toDisplay = Queue()
|
||||
self.threadpool = ThreadPool(max_workers=cpu_count())
|
||||
|
||||
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.update_ui_signal.emit()
|
||||
|
||||
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("[error] {} could not be compressed".format(image.fullpath), file=sys.stderr)
|
||||
|
||||
|
||||
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)
|
||||
self.quitAction.triggered.connect(self.parent.close)
|
||||
|
||||
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)
|
||||
self.addFiles.triggered.connect(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)
|
||||
|
||||
self.addFiles.triggered.connect(self.parent.recompress_files)
|
||||
|
||||
self.hideMain = QAction(self.tr("&Hide window"), self)
|
||||
self.hideMain.triggered.connect(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 = StartQt()
|
||||
|
||||
if myapp.showapp:
|
||||
myapp.show()
|
||||
sys.exit(app.exec_())
|
||||
|
|
@ -1,15 +1,23 @@
|
|||
from PyQt4.QtCore import *
|
||||
from PyQt4.QtGui import *
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from os import path
|
||||
|
||||
from PyQt5.QtCore import *
|
||||
from PyQt5.QtGui import *
|
||||
from PyQt5.QtWidgets import *
|
||||
|
||||
|
||||
class TrimageTableView(QTableView):
|
||||
|
||||
drop_event_signal = pyqtSignal(list)
|
||||
|
||||
"""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().hasFormat("text/uri-list"):
|
||||
if event.mimeData().hasUrls:
|
||||
event.accept()
|
||||
else:
|
||||
event.ignore()
|
||||
|
|
@ -18,23 +26,33 @@ class TrimageTableView(QTableView):
|
|||
event.accept()
|
||||
|
||||
def dropEvent(self, event):
|
||||
files = str(event.mimeData().data("text/uri-list")).strip().split()
|
||||
for i, file in enumerate(files):
|
||||
files[i] = QUrl(QString(file)).toLocalFile()
|
||||
self.emit(SIGNAL("fileDropEvent"), (files))
|
||||
event.accept()
|
||||
filelist = []
|
||||
for url in event.mimeData().urls():
|
||||
filelist.append(url.toLocalFile())
|
||||
|
||||
self.drop_event_signal.emit(filelist)
|
||||
|
||||
|
||||
class Ui_trimage(object):
|
||||
class Ui_trimage():
|
||||
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)
|
||||
trimage.setWindowIcon(QIcon("trimage-icon.png"))
|
||||
|
||||
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.setContentsMargins(0, 0, 0, 0)
|
||||
self.gridLayout_2.setSpacing(0)
|
||||
self.gridLayout_2.setObjectName("gridLayout_2")
|
||||
|
||||
|
|
@ -50,7 +68,7 @@ class Ui_trimage(object):
|
|||
|
||||
self.verticalLayout = QVBoxLayout(self.widget)
|
||||
self.verticalLayout.setSpacing(0)
|
||||
self.verticalLayout.setMargin(0)
|
||||
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
|
||||
self.frame = QFrame(self.widget)
|
||||
|
|
@ -58,12 +76,12 @@ class Ui_trimage(object):
|
|||
|
||||
self.verticalLayout_2 = QVBoxLayout(self.frame)
|
||||
self.verticalLayout_2.setSpacing(0)
|
||||
self.verticalLayout_2.setMargin(0)
|
||||
self.verticalLayout_2.setContentsMargins(0, 0, 0, 0)
|
||||
self.verticalLayout_2.setObjectName("verticalLayout_2")
|
||||
|
||||
self.horizontalLayout = QHBoxLayout()
|
||||
self.horizontalLayout.setSpacing(0)
|
||||
self.horizontalLayout.setMargin(10)
|
||||
self.horizontalLayout.setContentsMargins(10, 10, 10, 10)
|
||||
self.horizontalLayout.setObjectName("horizontalLayout")
|
||||
|
||||
self.addfiles = QPushButton(self.frame)
|
||||
|
|
@ -72,7 +90,7 @@ class Ui_trimage(object):
|
|||
self.addfiles.setFont(font)
|
||||
self.addfiles.setCursor(Qt.PointingHandCursor)
|
||||
icon = QIcon()
|
||||
icon.addPixmap(QPixmap("list-add.png"), QIcon.Normal, QIcon.Off)
|
||||
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)
|
||||
|
|
@ -83,7 +101,7 @@ class Ui_trimage(object):
|
|||
font.setPointSize(8)
|
||||
self.label.setFont(font)
|
||||
self.label.setFrameShadow(QFrame.Plain)
|
||||
self.label.setMargin(1)
|
||||
self.label.setContentsMargins(1, 1, 1, 1)
|
||||
self.label.setIndent(10)
|
||||
self.label.setObjectName("label")
|
||||
self.horizontalLayout.addWidget(self.label)
|
||||
|
|
@ -98,7 +116,7 @@ class Ui_trimage(object):
|
|||
self.recompress.setCursor(Qt.PointingHandCursor)
|
||||
|
||||
icon1 = QIcon()
|
||||
icon1.addPixmap(QPixmap("view-refresh.png"), QIcon.Normal, QIcon.Off)
|
||||
icon1.addPixmap(QPixmap(self.get_image("pixmaps/view-refresh.png")), QIcon.Normal, QIcon.Off)
|
||||
|
||||
self.recompress.setIcon(icon1)
|
||||
self.recompress.setCheckable(False)
|
||||
|
|
@ -131,25 +149,24 @@ class Ui_trimage(object):
|
|||
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))
|
||||
"Trimage image compressor", None))
|
||||
self.addfiles.setToolTip(QApplication.translate("trimage",
|
||||
"Add file to the compression list", None,
|
||||
QApplication.UnicodeUTF8))
|
||||
"Add file to the compression list", None))
|
||||
self.addfiles.setText(QApplication.translate("trimage",
|
||||
"&Add and compress", None, QApplication.UnicodeUTF8))
|
||||
"&Add and compress", None))
|
||||
self.addfiles.setShortcut(QApplication.translate("trimage",
|
||||
"Alt+A", None, QApplication.UnicodeUTF8))
|
||||
"Alt+A", None))
|
||||
self.label.setText(QApplication.translate("trimage",
|
||||
"Drag and drop images onto the table", None,
|
||||
QApplication.UnicodeUTF8))
|
||||
"Drag and drop images onto the table", None))
|
||||
self.recompress.setToolTip(QApplication.translate("trimage",
|
||||
"Recompress selected images", None, QApplication.UnicodeUTF8))
|
||||
"Recompress all images", None))
|
||||
self.recompress.setText(QApplication.translate("trimage",
|
||||
"&Recompress", None, QApplication.UnicodeUTF8))
|
||||
"&Recompress", None))
|
||||
self.recompress.setShortcut(QApplication.translate("trimage",
|
||||
"Alt+R", None, QApplication.UnicodeUTF8))
|
||||
"Alt+R", None))
|
||||
self.processedfiles.setToolTip(QApplication.translate("trimage",
|
||||
"Drag files in here", None, QApplication.UnicodeUTF8))
|
||||
"Drag files in here", None))
|
||||
self.processedfiles.setWhatsThis(QApplication.translate("trimage",
|
||||
"Drag files in here", None, QApplication.UnicodeUTF8))
|
||||
"Drag files in here", None))
|
||||
BIN
website/arch.png
Normal file
|
After Width: | Height: | Size: 1 KiB |
BIN
website/debian.png
Normal file
|
After Width: | Height: | Size: 356 B |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 23 KiB |
|
|
@ -1,13 +1,18 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<title>Trimage image compressor</title>
|
||||
<link href="http://github.com/Kilian/sencss/raw/master/source/sen.css" rel="stylesheet" type="text/css">
|
||||
<meta charset="utf8" />
|
||||
<title>Trimage (lossless) image compressor</title>
|
||||
<link
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<style>
|
||||
body {
|
||||
background:#fff
|
||||
font-size:13px;
|
||||
font-family:sans-serif;
|
||||
}
|
||||
#wrap {
|
||||
position:relative;
|
||||
|
|
@ -40,7 +45,8 @@
|
|||
.tri {
|
||||
-moz-column-count:3;
|
||||
-webkit-column-count:3;
|
||||
text-align:justify;
|
||||
-moz-column-gap:2em;
|
||||
-webkit-column-gap:2em;
|
||||
}
|
||||
img {
|
||||
margin-bottom:1.5em;
|
||||
|
|
@ -68,66 +74,117 @@
|
|||
|
||||
<body>
|
||||
<div id="wrap">
|
||||
<h1><img src="trimage-icon.png" alt=""> Trimage image compressor – 1.0.0b (beta)</h1>
|
||||
<span class="subtitle">A cross-platform tool for optimizing PNG and JPG files.</span>
|
||||
<p class="tri">Trimage is a cross-platform GUI and command-line interface to optimize image
|
||||
files via <a href="http://optipng.sourceforge.net/">optipng</a>,
|
||||
<a href="http://advancemame.sourceforge.net/comp-readme.html">advpng</a> and
|
||||
<a href="http://www.kokkonen.net/tjko/projects.html">jpegoptim</a>, depending on the filetype
|
||||
(currently, PNG and JPG files are supported). It was inspired by
|
||||
<a href="http://imageoptim.pornel.net/">imageoptim</a>. 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.</p>
|
||||
<h1>
|
||||
<img src="trimage-icon.png" alt="" /> Trimage image compressor –
|
||||
1.0.6
|
||||
</h1>
|
||||
<span class="subtitle"
|
||||
>A cross-platform tool for losslessly optimizing PNG and JPG files for
|
||||
web.</span
|
||||
>
|
||||
<p class="tri">
|
||||
Trimage is a cross-platform GUI and command-line interface to optimize
|
||||
image files for websites, using
|
||||
<a href="http://optipng.sourceforge.net/">optipng</a>,
|
||||
<a href="http://pmt.sourceforge.net/pngcrush/">pngcrush</a>,
|
||||
<a href="http://advancemame.sourceforge.net/comp-readme.html">advpng</a>
|
||||
and <a href="http://www.kokkonen.net/tjko/projects.html">jpegoptim</a>,
|
||||
depending on the filetype (currently, PNG and JPG files are supported).
|
||||
It was inspired by
|
||||
<a href="http://imageoptim.pornel.net/">imageoptim</a>. All image files
|
||||
are losslessy compressed on the highest available compression levels,
|
||||
and EXIF and other metadata is removed. Trimage gives you various input
|
||||
functions to fit your own workflow: A regular file dialog, dragging and
|
||||
dropping and various command line options.
|
||||
</p>
|
||||
|
||||
<h2>Trimage in action</h2>
|
||||
<img src="image.png" alt="a screenshot of Trimage">
|
||||
<img src="image.png" alt="a screenshot of Trimage" />
|
||||
|
||||
<div class="block2">
|
||||
<h2>Download</h2>
|
||||
<h3><img src="ubuntu.png" alt=""> Ubuntu</h3>
|
||||
|
||||
<h3><img src="debian.png" alt="" /> Debian (sid)</h3>
|
||||
<p>Trimage is available in the official Debian Sid repositories:</p>
|
||||
<ol>
|
||||
<li><code>sudo add-apt-repository lp:trimage</code></li>
|
||||
<li><code>sudo apt-get install trimage</code></li>
|
||||
</ol>
|
||||
|
||||
<h3><img src="linux.png" alt=""> Other *nix</h3>
|
||||
<h3><img src="ubuntu.png" alt="" /> Ubuntu</h3>
|
||||
<p>Trimage is available in the official repositories:</p>
|
||||
<a href="https://apps.ubuntu.com/cat/applications/trimage/"
|
||||
>Download for Ubuntu</a
|
||||
>
|
||||
<p>Alternatively:</p>
|
||||
<ol>
|
||||
<li><code>sudo apt-get install trimage</code></li>
|
||||
</ol>
|
||||
|
||||
<h3><img src="arch.png" alt="" /> Arch Linux</h3>
|
||||
Trimage is available from AUR, to install, type:
|
||||
<ol>
|
||||
<li><code>yaourt -S trimage</code></li>
|
||||
</ol>
|
||||
|
||||
<h3><img src="macos.png" alt="" /> macOS</h3><!-- Attribution for the image: VICDJES21 / CC BY-SA (https://creativecommons.org/licenses/by-sa/4.0) -->
|
||||
Trimage is available from Homebrew, to install, type:
|
||||
<ol>
|
||||
<li><code>brew install trimage</code></li>
|
||||
</ol>
|
||||
<p>Launch by executing <code>trimage</code> in Terminal.app</p>
|
||||
|
||||
<h3><img src="linux.png" alt="" /> Other *nix</h3>
|
||||
<ol>
|
||||
<li>Download the source via git or bzr (see repositories)</li>
|
||||
<li>Make sure you have all the requirements installed (see requirements)</li>
|
||||
<li>make trimage.py executable (<code>chmod +x trimage.py</code>)</li>
|
||||
<li>Start by clicking on trimage.py or launching it with <code>./trimage.py</code></li>
|
||||
<li>
|
||||
Make sure you have all the requirements installed (see requirements)
|
||||
</li>
|
||||
<li>Enter <code>python setup.py install</code> into your console</li>
|
||||
<li>Launch by executing <code>trimage</code></li>
|
||||
</ol>
|
||||
<p>Help us make .deb's, rpms's etc: <a href="mailto:help@trimage.org">contact us</a></p>
|
||||
<h3><img src="mac.png" alt=""> Mac</h3>
|
||||
<p>Trimage should be able to run on Mac. <a href="mailto:help@trimage.org">Help us with this</a></p>
|
||||
<p>
|
||||
Help us make .snaps, .appimages etc:
|
||||
<a href="mailto:help@trimage.org">contact us</a> or
|
||||
<a href="https://github.com/kilian/trimage/pulls"
|
||||
>open a pull request</a
|
||||
>
|
||||
</p>
|
||||
<!--
|
||||
<h3><img src="windows.png" alt=""> Windows</h3>
|
||||
<p>Trimage should be able to run on Windows. <a href="mailto:help@trimage.org">Help us with this</a></p>
|
||||
-->
|
||||
</div>
|
||||
<div class="block">
|
||||
<h2>Repositories</h2>
|
||||
<p><strong>Git:</strong> Trimage is primarily developed on <a href="http://github.com/Kilian/Trimage">GitHub</a>.</p>
|
||||
<p><strong>Bzr:</strong> Trimage is also available on <a href="https://launchpad.net/trimage">Launchpad</a>.</p>
|
||||
<p>
|
||||
<strong>Git:</strong> Trimage is primarily developed on
|
||||
<a href="http://github.com/Kilian/Trimage">GitHub</a>.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Bzr:</strong> Trimage is also available on
|
||||
<a href="https://launchpad.net/trimage">Launchpad</a>.
|
||||
</p>
|
||||
<p>Trimage is MIT licenced. We encourage contributions via GitHub.</p>
|
||||
</div>
|
||||
<div class="block">
|
||||
<h2>Thanks</h2>
|
||||
<p>The following people helped develop Trimage:</p>
|
||||
<ul>
|
||||
<li><a href="https://hugo-posnic.fr/">Hugo Posnic</a></li>
|
||||
<li>Neil Wallace</li>
|
||||
<li>Jeroen Goudsmit</li>
|
||||
<li>Tarnay Kálmán</li>
|
||||
<li>Thomas Lété</li>
|
||||
<li>Kyrill Detinov</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="block">
|
||||
<h2>Donate</h2>
|
||||
<p>If you enjoy using Trimage, please buy us a coffee or a beer :)</p>
|
||||
<a href='http://www.pledgie.com/campaigns/9607'><img alt='Click here to lend your support to: Trimage and make a donation at www.pledgie.com !' src='http://www.pledgie.com/campaigns/9607.png?skin_name=chrome' border='0' /></a>
|
||||
</div>
|
||||
<div class="block">
|
||||
<h2>Requirements</h2>
|
||||
<ul>
|
||||
<li>python <em>2.6</em></li>
|
||||
<li>python-qt4 <em>4.4</em></li>
|
||||
<li>python <em>3</em></li>
|
||||
<li>python-qt5 <em>5</em></li>
|
||||
<li>optipng <em>0.6.2.1</em></li>
|
||||
<li>pngcrush <em>1.6.7</em></li>
|
||||
<li>advancecomp <em>1.15</em></li>
|
||||
<li>jpegoptim <em>1.2.2</em></li>
|
||||
</ul>
|
||||
|
|
@ -152,29 +209,18 @@
|
|||
</div>
|
||||
<div class="block">
|
||||
<h2>Help</h2>
|
||||
<p>Please use <a href="http://github.com/Kilian/Trimage">GitHub</a> for reporting bugs and email <a href="mailto:help@trimage.org">help@trimage.org</a> for general questions (though we are not a helpdesk!)</p>
|
||||
<p>
|
||||
Please use <a href="https://github.com/Kilian/Trimage">GitHub</a> for
|
||||
reporting bugs and email
|
||||
<a href="mailto:help@trimage.org">help@trimage.org</a> for general
|
||||
questions (though we are not a helpdesk!)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2>Planned features</h2>
|
||||
<p>Version 1.0.0 final:</p>
|
||||
<ul>
|
||||
<li>Expand command line options</li>
|
||||
<li>Make sure a compressed file is always smaller than the original one, or don't compress</li>
|
||||
<li>General refactoring</li>
|
||||
</ul>
|
||||
<p>Version 1.1.0</p>
|
||||
<ul>
|
||||
<li>Use multiprocessing instead of threading</li>
|
||||
</ul>
|
||||
<p>Beyond that</p>
|
||||
<ul>
|
||||
<li>Deletion of rows in the table view</li>
|
||||
<li>Notification area widget</li>
|
||||
<li>Integration with online services such as punypng</li>
|
||||
<li><a href="mailto:help@trimage.org">Suggest something</a></li>
|
||||
</ul>
|
||||
<p class="footer">Trimage is © 2010 <a href="http://kilianvalkhof.com">Kilian Valkhof</a>, <a href="http://paulchaplin.com">Paul Chaplin</a>
|
||||
<p class="footer">
|
||||
Trimage is © 2010–2019
|
||||
<a href="http://kilianvalkhof.com">Kilian Valkhof</a>,
|
||||
<a href="http://paulchaplin.com">Paul Chaplin</a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.1 KiB |
BIN
website/mac.png
|
Before Width: | Height: | Size: 968 B |
BIN
website/macos.png
Normal file
|
After Width: | Height: | Size: 835 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |