diff --git a/.travis.yml b/.travis.yml
index b1ba591..0207f88 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -34,7 +34,7 @@ matrix:
language: generic
osx_image: xcode11 # Python 3.7.4 running on macOS 10.14.4
before_install:
- - brew install pngcheck
+ - export HOMEBREW_NO_INSTALL_CLEANUP=1 && brew install pngcheck
install:
- make build-dependencies
- make install-executable
@@ -56,6 +56,17 @@ matrix:
script:
- flake8 --ignore=E501,W503,E121,E123,E126,E226,E24,E704,W503,W504 src/crunch.py
- shellcheck --exclude=2046 src/*.sh
+ - name: "Benchmarks"
+ python: 3.7
+ env: TOX_ENV=py37
+ dist: xenial
+ install:
+ - pip install --upgrade numpy
+ install:
+ - make build-dependencies
+ - make install-executable
+ script:
+ - make benchmark
# The following prevents Travis from running CI on pull requests that come from a
# branch in the same repository. Without this, it will run the same CI for the
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0216ed6..1413119 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,25 @@
## Changelog
+### v4.0.0
+
+- Updated pngquant to v2.12.5
+- Updated libpng to v1.6.37
+- Updated zopflipng to v2.2.0 (@chrissimpkins derivative) - upstream updates through https://github.com/google/zopfli/commit/5d9b71b3c636e9e14a8f7a3f983ff93a1a3793ac
+- crunch executable : added ANSI color support in stdout / stderr messages
+- crunch.py : PEP 8 source code formatting refactor with `black`
+- crunch.py : refactor logging setup approach
+- FIX Crunch macOS service : fixed bug in processing of png image file paths that include spaces (thanks Changyoung!)
+- FIX crunch executable: command line error handling when no arguments are passed to the command line `crunch` executable
+- Added Makefile dist target
+- Added Makefile benchmark target
+- Added Makefile clean target
+- Updated Makefile flake8 linting target
+- Updated dmg-builder.sh dmg installer script
+- Added new image-compare.py script for comparison of test image file sizes
+- Added new dssim-comparisons.sh script for DSSIM analysis of pre/post compression test images
+- Added new suite of reference PNG images and benchmarking support in `bench.py` script
+- Added continuous benchmarking through Travis CI
+
### v3.0.1
- modified the macOS GUI idle animation to reduce CPU usage during the application idle stage (issue report #66)
diff --git a/LICENSE.md b/LICENSE.md
index 12dda10..24b21c3 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -2,7 +2,7 @@
The MIT License (MIT)
-Copyright (c) 2018 Christopher Simpkins
+Copyright (c) 2019 Christopher Simpkins
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/Makefile b/Makefile
index 4bdb419..fc9ec76 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,19 @@
+benchmark:
+ cd benchmarks && $(MAKE) $@
+
build-dependencies:
src/install-dependencies.sh
+clean:
+ rm benchmarks/img/*-crunch.png
+
+dist:
+ ./dmg-builder.sh
+
+dist-homebrew:
+ cask-repair crunch
+
install-executable:
sudo cp src/crunch.py /usr/local/bin/crunch
@echo " "
@@ -37,7 +49,7 @@ test-coverage:
test-python:
tox
- flake8 --ignore=E501,W503,E121,E123,E126,E226,E24,E704,W503,W504 src/crunch.py
+ flake8 --ignore=E501,W503,E121,E123,E126,E226,E24,E704,W503,W504,N806 src/crunch.py
test-shell:
shellcheck --exclude=2046 src/*.sh
@@ -49,4 +61,5 @@ test-valid-png-output:
test: test-python test-shell test-valid-png-output
-.PHONY: build-dependencies install-executable install-macos-service uninstall-executable uninstall-macos-service test test-coverage test-python test-shell test-valid-png-output
\ No newline at end of file
+
+.PHONY: benchmark build-dependencies install-executable install-macos-service uninstall-executable uninstall-macos-service test test-coverage test-python test-shell test-valid-png-output dist
\ No newline at end of file
diff --git a/README.md b/README.md
index f9323d7..3585eff 100644
--- a/README.md
+++ b/README.md
@@ -2,23 +2,23 @@
[![GitHub release](https://img.shields.io/github/release/chrissimpkins/Crunch.svg?style=flat-square)](https://github.com/chrissimpkins/Crunch/releases/latest)
-[![Build Status](https://semaphoreci.com/api/v1/sourcefoundry/crunch/branches/master/badge.svg)](https://semaphoreci.com/sourcefoundry/crunch)
+[![Build Status](https://travis-ci.com/chrissimpkins/Crunch.svg?branch=master)](https://travis-ci.com/chrissimpkins/Crunch)
## About
Crunch is a tool for lossy PNG image file optimization. It combines selective bit depth, color type, and color palette reduction with zopfli DEFLATE compression algorithm encoding using the pngquant and zopflipng PNG optimization tools. This approach leads to a significant file size gain relative to lossless approaches at the expense of a relatively modest decrease in image quality (see [example images](#examples) below).
-Historical benchmarks with the files included in Cédric Louvrier's [PNG Test Corpus](https://css-ig.net/png-tools-overview) versus other commonly used PNG optimization software are available in [BENCHMARKS.md](docs/BENCHMARKS.md).
+Continuous benchmark testing is available on [Travis CI](https://travis-ci.com/chrissimpkins/Crunch) (open the Benchmarks build for the commit of interest). Please see the benchmarks directory of this repository for details about the benchmarking approach and instructions on how to execute benchmarks locally on the reference images distributed in this repository or with your own image files.
-Crunch PNG image optimization is available through the following applications in this repository:
+Crunch PNG image optimization is available through the following applications that are distributed in this repository:
-- [`crunch`](docs/EXECUTABLE.md) - a *nix command line executable that can be used on macOS, Linux, and Windows POSIX application deployment environments such as Cygwin
+- [`crunch`](docs/EXECUTABLE.md) - a *nix command line executable that can be used on macOS, Linux, and Windows POSIX application deployment environments such as Cygwin or the Windows subsystem for Linux
- [Crunch GUI](docs/MACOSGUI.md) - a native macOS drag and drop GUI tool
- [Crunch Image(s)](docs/SERVICE.md) service - a macOS right-click menu service for PNG images selected in the Finder
-## Install and Usage
+## Installation and Usage
-Install and usage documentation links for each of the Crunch applications are available below.
+Installation and usage documentation links for each of the Crunch applications are available below.
## `crunch` Command Line Executable
@@ -50,7 +50,7 @@ Select one or more PNG images in the Finder, right-click, and select the `Servic
## Examples
-The following examples demonstrate the benefits and disadvantages of the current iteration of Crunch's aggressive space saving optimization strategy. In many cases, the PNG optimization minimizes file size with an imperceptible decrease in image quality. In some cases, degradation of image quality is visible. View the horizon line in the prairie photo below for a demonstration of an undesirable artifact that is introduced with image processing. Experiment with the image types that you use and please submit a report with examples of any images where the image quality falls short of expectations for production-ready files.
+The following examples demonstrate the benefits and disadvantages of the current iteration of Crunch's aggressive space saving optimization strategy. The optimized image files are updated at every Crunch release. In many cases, the PNG optimization decreases file size with an imperceptible impact on image quality. In some cases, degradation of image quality is visible. Visual confirmation of image quality is highly recommended with lossy optimization tools in production settings.
## Photography Examples
@@ -58,7 +58,7 @@ The following examples demonstrate the benefits and disadvantages of the current
- Original Size: 583,398 bytes
- Optimized Size: 196,085 bytes
-- DSSIM similarity score: 0.001471
+- DSSIM similarity score: 0.001383
- Percent original size: 33.61%
##### Original
@@ -73,7 +73,7 @@ The following examples demonstrate the benefits and disadvantages of the current
- Original Size: 138,272
- Optimized Size: 66,593
-- DSSIM similarity score: 0.000948
+- DSSIM similarity score: 0.000920
- Percent original size: 48.16%
##### Original
@@ -89,7 +89,7 @@ The following examples demonstrate the benefits and disadvantages of the current
- Original Size: 196,794 bytes
- Optimized Size: 77,965 bytes
-- DSSIM similarity score: 0.002988
+- DSSIM similarity score: 0.002923
- Percent original size: 39.62%
##### Original
@@ -108,7 +108,7 @@ The following examples demonstrate the benefits and disadvantages of the current
- Original Size: 197,193 bytes
- Optimized Size: 67,596 bytes
-- DSSIM similarity score: 0.000162
+- DSSIM similarity score: 0.003047
- Percent original size: 34.28%
##### Original
@@ -123,7 +123,7 @@ The following examples demonstrate the benefits and disadvantages of the current
- Original Size: 249,251 bytes
- Optimized Size: 67,135 bytes
-- DSSIM similarity score: 0.002491
+- DSSIM similarity score: 0.002450
- Percent original size: 26.93%
##### Original
@@ -139,7 +139,7 @@ The following examples demonstrate the benefits and disadvantages of the current
- Original Size: 440,126 bytes
- Optimized Size: 196,962 bytes
-- DSSIM similarity score: 0.000480
+- DSSIM similarity score: 0.001013
- Percent original size: 44.75%
##### Original
@@ -152,6 +152,8 @@ The following examples demonstrate the benefits and disadvantages of the current
All images above were obtained from [Pixabay](https://pixabay.com) and are dedicated to the public domain under the [CC0 Public Domain Dedication](https://creativecommons.org/publicdomain/zero/1.0/).
+DSSIM testing was performed with v2.10.0 of the [kornelski/dssim tool](https://github.com/kornelski/dssim).
+
## Issue Reporting
Have you identified a problem? Please [create a new issue report](https://github.com/chrissimpkins/Crunch/issues/new/choose) on the Github issue tracker so that we can address it.
diff --git a/benchmarks/Makefile b/benchmarks/Makefile
new file mode 100644
index 0000000..03dacf9
--- /dev/null
+++ b/benchmarks/Makefile
@@ -0,0 +1,7 @@
+
+benchmark:
+ cd img && /usr/bin/time -p crunch *.png
+ cd img && python3 bench.py
+
+
+.PHONY: benchmark
\ No newline at end of file
diff --git a/benchmarks/PngSuite.LICENSE b/benchmarks/PngSuite.LICENSE
new file mode 100644
index 0000000..6f96ceb
--- /dev/null
+++ b/benchmarks/PngSuite.LICENSE
@@ -0,0 +1,8 @@
+PngSuite
+--------
+
+Permission to use, copy, modify and distribute these images for any
+purpose and without fee is hereby granted.
+
+
+(c) Willem van Schaik, 1996, 2011
diff --git a/benchmarks/README.md b/benchmarks/README.md
new file mode 100644
index 0000000..9312ef3
--- /dev/null
+++ b/benchmarks/README.md
@@ -0,0 +1,36 @@
+## Crunch Benchmarks
+
+This directory includes reference PNG files from http://www.schaik.com/pngsuite/ for benchmarking Crunch PNG optimization. The image files in this directory are distributed under the [PngSuite.LICENSE](PngSuite.LICENSE).
+
+Benchmarks are performed against all PNG image files released in the http://www.schaik.com/pngsuite/PngSuite-2017jul19.tgz archive *except* the [reference corrupted files](http://www.schaik.com/pngsuite/#corrupted).
+
+The [results of continuous benchmark testing are available on Travis CI in the "Benchmarks" build](https://travis-ci.org/chrissimpkins/Crunch).
+
+## How to Execute Benchmarks Locally
+
+Clone this repository, build the dependency tools, install the `crunch` executable, and run benchmarks with the following set of commands:
+
+```
+$ git clone https://github.com/chrissimpkins/Crunch.git
+$ make build-dependencies
+$ make install-executable
+$ make benchmark
+```
+
+You will see the results in your terminal.
+
+The optimized image files remain in the `benchmarks/img` subdirectory after execution for your review. To clean the optimized files, use the following command:
+
+```
+$ make clean
+```
+
+## How to Benchmark With Other Image Sets
+
+The Python 3 [`bench.py` script](img/bench.py) is portable. Download the script, drop it into the directory with your image files, execute `crunch` on all images in the directory, and execute the benchmark script with:
+
+```
+$ python3 bench.py
+```
+
+**Note**: The benchmarking script has a couple of requirements. Optimize **all files** that are included in the directory before you execute the script. Keep the original `*-crunch.png` file paths for all optimized image files.
diff --git a/benchmarks/img/basi0g01.png b/benchmarks/img/basi0g01.png
new file mode 100644
index 0000000..556fa72
Binary files /dev/null and b/benchmarks/img/basi0g01.png differ
diff --git a/benchmarks/img/basi0g02.png b/benchmarks/img/basi0g02.png
new file mode 100644
index 0000000..ce09821
Binary files /dev/null and b/benchmarks/img/basi0g02.png differ
diff --git a/benchmarks/img/basi0g04.png b/benchmarks/img/basi0g04.png
new file mode 100644
index 0000000..3853273
Binary files /dev/null and b/benchmarks/img/basi0g04.png differ
diff --git a/benchmarks/img/basi0g08.png b/benchmarks/img/basi0g08.png
new file mode 100644
index 0000000..faed8be
Binary files /dev/null and b/benchmarks/img/basi0g08.png differ
diff --git a/benchmarks/img/basi0g16.png b/benchmarks/img/basi0g16.png
new file mode 100644
index 0000000..a9f2816
Binary files /dev/null and b/benchmarks/img/basi0g16.png differ
diff --git a/benchmarks/img/basi2c08.png b/benchmarks/img/basi2c08.png
new file mode 100644
index 0000000..2aab44d
Binary files /dev/null and b/benchmarks/img/basi2c08.png differ
diff --git a/benchmarks/img/basi2c16.png b/benchmarks/img/basi2c16.png
new file mode 100644
index 0000000..cd7e50f
Binary files /dev/null and b/benchmarks/img/basi2c16.png differ
diff --git a/benchmarks/img/basi3p01.png b/benchmarks/img/basi3p01.png
new file mode 100644
index 0000000..00a7cea
Binary files /dev/null and b/benchmarks/img/basi3p01.png differ
diff --git a/benchmarks/img/basi3p02.png b/benchmarks/img/basi3p02.png
new file mode 100644
index 0000000..bb16b44
Binary files /dev/null and b/benchmarks/img/basi3p02.png differ
diff --git a/benchmarks/img/basi3p04.png b/benchmarks/img/basi3p04.png
new file mode 100644
index 0000000..b4e888e
Binary files /dev/null and b/benchmarks/img/basi3p04.png differ
diff --git a/benchmarks/img/basi3p08.png b/benchmarks/img/basi3p08.png
new file mode 100644
index 0000000..50a6d1c
Binary files /dev/null and b/benchmarks/img/basi3p08.png differ
diff --git a/benchmarks/img/basi4a08.png b/benchmarks/img/basi4a08.png
new file mode 100644
index 0000000..398132b
Binary files /dev/null and b/benchmarks/img/basi4a08.png differ
diff --git a/benchmarks/img/basi4a16.png b/benchmarks/img/basi4a16.png
new file mode 100644
index 0000000..51192e7
Binary files /dev/null and b/benchmarks/img/basi4a16.png differ
diff --git a/benchmarks/img/basi6a08.png b/benchmarks/img/basi6a08.png
new file mode 100644
index 0000000..aecb32e
Binary files /dev/null and b/benchmarks/img/basi6a08.png differ
diff --git a/benchmarks/img/basi6a16.png b/benchmarks/img/basi6a16.png
new file mode 100644
index 0000000..4181533
Binary files /dev/null and b/benchmarks/img/basi6a16.png differ
diff --git a/benchmarks/img/basn0g01.png b/benchmarks/img/basn0g01.png
new file mode 100644
index 0000000..1d72242
Binary files /dev/null and b/benchmarks/img/basn0g01.png differ
diff --git a/benchmarks/img/basn0g02.png b/benchmarks/img/basn0g02.png
new file mode 100644
index 0000000..5083324
Binary files /dev/null and b/benchmarks/img/basn0g02.png differ
diff --git a/benchmarks/img/basn0g04.png b/benchmarks/img/basn0g04.png
new file mode 100644
index 0000000..0bf3687
Binary files /dev/null and b/benchmarks/img/basn0g04.png differ
diff --git a/benchmarks/img/basn0g08.png b/benchmarks/img/basn0g08.png
new file mode 100644
index 0000000..23c8237
Binary files /dev/null and b/benchmarks/img/basn0g08.png differ
diff --git a/benchmarks/img/basn0g16.png b/benchmarks/img/basn0g16.png
new file mode 100644
index 0000000..e7c82f7
Binary files /dev/null and b/benchmarks/img/basn0g16.png differ
diff --git a/benchmarks/img/basn2c08.png b/benchmarks/img/basn2c08.png
new file mode 100644
index 0000000..db5ad15
Binary files /dev/null and b/benchmarks/img/basn2c08.png differ
diff --git a/benchmarks/img/basn2c16.png b/benchmarks/img/basn2c16.png
new file mode 100644
index 0000000..50c1cb9
Binary files /dev/null and b/benchmarks/img/basn2c16.png differ
diff --git a/benchmarks/img/basn3p01.png b/benchmarks/img/basn3p01.png
new file mode 100644
index 0000000..b145c2b
Binary files /dev/null and b/benchmarks/img/basn3p01.png differ
diff --git a/benchmarks/img/basn3p02.png b/benchmarks/img/basn3p02.png
new file mode 100644
index 0000000..8985b3d
Binary files /dev/null and b/benchmarks/img/basn3p02.png differ
diff --git a/benchmarks/img/basn3p04.png b/benchmarks/img/basn3p04.png
new file mode 100644
index 0000000..0fbf9e8
Binary files /dev/null and b/benchmarks/img/basn3p04.png differ
diff --git a/benchmarks/img/basn3p08.png b/benchmarks/img/basn3p08.png
new file mode 100644
index 0000000..0ddad07
Binary files /dev/null and b/benchmarks/img/basn3p08.png differ
diff --git a/benchmarks/img/basn4a08.png b/benchmarks/img/basn4a08.png
new file mode 100644
index 0000000..3e13052
Binary files /dev/null and b/benchmarks/img/basn4a08.png differ
diff --git a/benchmarks/img/basn4a16.png b/benchmarks/img/basn4a16.png
new file mode 100644
index 0000000..8243644
Binary files /dev/null and b/benchmarks/img/basn4a16.png differ
diff --git a/benchmarks/img/basn6a08.png b/benchmarks/img/basn6a08.png
new file mode 100644
index 0000000..e608738
Binary files /dev/null and b/benchmarks/img/basn6a08.png differ
diff --git a/benchmarks/img/basn6a16.png b/benchmarks/img/basn6a16.png
new file mode 100644
index 0000000..984a995
Binary files /dev/null and b/benchmarks/img/basn6a16.png differ
diff --git a/benchmarks/img/bench.py b/benchmarks/img/bench.py
new file mode 100644
index 0000000..3df643c
--- /dev/null
+++ b/benchmarks/img/bench.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python3
+
+# ----------------------------------------
+#
+# Copyright (c) 2019 Christopher Simpkins
+# MIT license
+#
+# ---------------------------------------
+
+
+import glob
+import os
+
+
+def grouped(iterable, n):
+ return zip(*[iter(iterable)] * n)
+
+
+paths = sorted(glob.glob("*.png"))
+
+percent_list = []
+pre_size_list = []
+post_size_list = []
+
+for path_a, path_b in grouped(paths, 2):
+ if "-crunch" in path_a:
+ post_path = path_a
+ pre_path = path_b
+ else:
+ post_path = path_b
+ pre_path = path_a
+
+ # assert that we are testing the correct pairs of files
+ assert f"{pre_path[:-4]}-crunch.png" == post_path
+
+ pre_size = os.path.getsize(pre_path)
+ post_size = os.path.getsize(post_path)
+ percent_size = (post_size / pre_size) * 100
+
+ percent_list.append(percent_size)
+ pre_size_list.append(pre_size)
+ post_size_list.append(post_size)
+
+ print(f"{post_path}: {percent_size:.2f}%")
+
+
+mean = sum(percent_list) / len(percent_list)
+total_initial_size = sum(pre_size_list)
+total_final_size = sum(post_size_list)
+delta = total_initial_size - total_final_size
+
+print(f"\nInitial:\t{total_initial_size:>8} B")
+print(f"Final: \t{total_final_size:>8} B")
+print(f"Delta: -{delta} B")
+print(f"Mean: {mean:.2f}%")
+try:
+ import numpy as np
+
+ a = np.array(percent_list)
+ stdev = np.std(a, dtype=np.float64)
+ print(f"SD: {stdev:.2f}%")
+except Exception:
+ pass
diff --git a/benchmarks/img/bgai4a08.png b/benchmarks/img/bgai4a08.png
new file mode 100644
index 0000000..398132b
Binary files /dev/null and b/benchmarks/img/bgai4a08.png differ
diff --git a/benchmarks/img/bgai4a16.png b/benchmarks/img/bgai4a16.png
new file mode 100644
index 0000000..51192e7
Binary files /dev/null and b/benchmarks/img/bgai4a16.png differ
diff --git a/benchmarks/img/bgan6a08.png b/benchmarks/img/bgan6a08.png
new file mode 100644
index 0000000..e608738
Binary files /dev/null and b/benchmarks/img/bgan6a08.png differ
diff --git a/benchmarks/img/bgan6a16.png b/benchmarks/img/bgan6a16.png
new file mode 100644
index 0000000..984a995
Binary files /dev/null and b/benchmarks/img/bgan6a16.png differ
diff --git a/benchmarks/img/bgbn4a08.png b/benchmarks/img/bgbn4a08.png
new file mode 100644
index 0000000..7cbefc3
Binary files /dev/null and b/benchmarks/img/bgbn4a08.png differ
diff --git a/benchmarks/img/bggn4a16.png b/benchmarks/img/bggn4a16.png
new file mode 100644
index 0000000..13fd85b
Binary files /dev/null and b/benchmarks/img/bggn4a16.png differ
diff --git a/benchmarks/img/bgwn6a08.png b/benchmarks/img/bgwn6a08.png
new file mode 100644
index 0000000..a67ff20
Binary files /dev/null and b/benchmarks/img/bgwn6a08.png differ
diff --git a/benchmarks/img/bgyn6a16.png b/benchmarks/img/bgyn6a16.png
new file mode 100644
index 0000000..ae3e9be
Binary files /dev/null and b/benchmarks/img/bgyn6a16.png differ
diff --git a/benchmarks/img/ccwn2c08.png b/benchmarks/img/ccwn2c08.png
new file mode 100644
index 0000000..47c2481
Binary files /dev/null and b/benchmarks/img/ccwn2c08.png differ
diff --git a/benchmarks/img/ccwn3p08.png b/benchmarks/img/ccwn3p08.png
new file mode 100644
index 0000000..8bb2c10
Binary files /dev/null and b/benchmarks/img/ccwn3p08.png differ
diff --git a/benchmarks/img/cdfn2c08.png b/benchmarks/img/cdfn2c08.png
new file mode 100644
index 0000000..559e526
Binary files /dev/null and b/benchmarks/img/cdfn2c08.png differ
diff --git a/benchmarks/img/cdhn2c08.png b/benchmarks/img/cdhn2c08.png
new file mode 100644
index 0000000..3e07e8e
Binary files /dev/null and b/benchmarks/img/cdhn2c08.png differ
diff --git a/benchmarks/img/cdsn2c08.png b/benchmarks/img/cdsn2c08.png
new file mode 100644
index 0000000..076c32c
Binary files /dev/null and b/benchmarks/img/cdsn2c08.png differ
diff --git a/benchmarks/img/cdun2c08.png b/benchmarks/img/cdun2c08.png
new file mode 100644
index 0000000..846033b
Binary files /dev/null and b/benchmarks/img/cdun2c08.png differ
diff --git a/benchmarks/img/ch1n3p04.png b/benchmarks/img/ch1n3p04.png
new file mode 100644
index 0000000..17cd12d
Binary files /dev/null and b/benchmarks/img/ch1n3p04.png differ
diff --git a/benchmarks/img/ch2n3p08.png b/benchmarks/img/ch2n3p08.png
new file mode 100644
index 0000000..25c1798
Binary files /dev/null and b/benchmarks/img/ch2n3p08.png differ
diff --git a/benchmarks/img/cm0n0g04.png b/benchmarks/img/cm0n0g04.png
new file mode 100644
index 0000000..9fba5db
Binary files /dev/null and b/benchmarks/img/cm0n0g04.png differ
diff --git a/benchmarks/img/cm7n0g04.png b/benchmarks/img/cm7n0g04.png
new file mode 100644
index 0000000..f7dc46e
Binary files /dev/null and b/benchmarks/img/cm7n0g04.png differ
diff --git a/benchmarks/img/cm9n0g04.png b/benchmarks/img/cm9n0g04.png
new file mode 100644
index 0000000..dd70911
Binary files /dev/null and b/benchmarks/img/cm9n0g04.png differ
diff --git a/benchmarks/img/cs3n2c16.png b/benchmarks/img/cs3n2c16.png
new file mode 100644
index 0000000..bf5fd20
Binary files /dev/null and b/benchmarks/img/cs3n2c16.png differ
diff --git a/benchmarks/img/cs3n3p08.png b/benchmarks/img/cs3n3p08.png
new file mode 100644
index 0000000..f4a6623
Binary files /dev/null and b/benchmarks/img/cs3n3p08.png differ
diff --git a/benchmarks/img/cs5n2c08.png b/benchmarks/img/cs5n2c08.png
new file mode 100644
index 0000000..40f947c
Binary files /dev/null and b/benchmarks/img/cs5n2c08.png differ
diff --git a/benchmarks/img/cs5n3p08.png b/benchmarks/img/cs5n3p08.png
new file mode 100644
index 0000000..dfd6e6e
Binary files /dev/null and b/benchmarks/img/cs5n3p08.png differ
diff --git a/benchmarks/img/cs8n2c08.png b/benchmarks/img/cs8n2c08.png
new file mode 100644
index 0000000..8e01d32
Binary files /dev/null and b/benchmarks/img/cs8n2c08.png differ
diff --git a/benchmarks/img/cs8n3p08.png b/benchmarks/img/cs8n3p08.png
new file mode 100644
index 0000000..a44066e
Binary files /dev/null and b/benchmarks/img/cs8n3p08.png differ
diff --git a/benchmarks/img/ct0n0g04.png b/benchmarks/img/ct0n0g04.png
new file mode 100644
index 0000000..40d1e06
Binary files /dev/null and b/benchmarks/img/ct0n0g04.png differ
diff --git a/benchmarks/img/ct1n0g04.png b/benchmarks/img/ct1n0g04.png
new file mode 100644
index 0000000..3ba110a
Binary files /dev/null and b/benchmarks/img/ct1n0g04.png differ
diff --git a/benchmarks/img/cten0g04.png b/benchmarks/img/cten0g04.png
new file mode 100644
index 0000000..a6a56fa
Binary files /dev/null and b/benchmarks/img/cten0g04.png differ
diff --git a/benchmarks/img/ctfn0g04.png b/benchmarks/img/ctfn0g04.png
new file mode 100644
index 0000000..353873e
Binary files /dev/null and b/benchmarks/img/ctfn0g04.png differ
diff --git a/benchmarks/img/ctgn0g04.png b/benchmarks/img/ctgn0g04.png
new file mode 100644
index 0000000..453f2b0
Binary files /dev/null and b/benchmarks/img/ctgn0g04.png differ
diff --git a/benchmarks/img/cthn0g04.png b/benchmarks/img/cthn0g04.png
new file mode 100644
index 0000000..8fce253
Binary files /dev/null and b/benchmarks/img/cthn0g04.png differ
diff --git a/benchmarks/img/ctjn0g04.png b/benchmarks/img/ctjn0g04.png
new file mode 100644
index 0000000..a77b8d2
Binary files /dev/null and b/benchmarks/img/ctjn0g04.png differ
diff --git a/benchmarks/img/ctzn0g04.png b/benchmarks/img/ctzn0g04.png
new file mode 100644
index 0000000..b4401c9
Binary files /dev/null and b/benchmarks/img/ctzn0g04.png differ
diff --git a/benchmarks/img/exif2c08.png b/benchmarks/img/exif2c08.png
new file mode 100644
index 0000000..56eb734
Binary files /dev/null and b/benchmarks/img/exif2c08.png differ
diff --git a/benchmarks/img/f00n0g08.png b/benchmarks/img/f00n0g08.png
new file mode 100644
index 0000000..45a0075
Binary files /dev/null and b/benchmarks/img/f00n0g08.png differ
diff --git a/benchmarks/img/f00n2c08.png b/benchmarks/img/f00n2c08.png
new file mode 100644
index 0000000..d6a1fff
Binary files /dev/null and b/benchmarks/img/f00n2c08.png differ
diff --git a/benchmarks/img/f01n0g08.png b/benchmarks/img/f01n0g08.png
new file mode 100644
index 0000000..4a1107b
Binary files /dev/null and b/benchmarks/img/f01n0g08.png differ
diff --git a/benchmarks/img/f01n2c08.png b/benchmarks/img/f01n2c08.png
new file mode 100644
index 0000000..26fee95
Binary files /dev/null and b/benchmarks/img/f01n2c08.png differ
diff --git a/benchmarks/img/f02n0g08.png b/benchmarks/img/f02n0g08.png
new file mode 100644
index 0000000..bfe410c
Binary files /dev/null and b/benchmarks/img/f02n0g08.png differ
diff --git a/benchmarks/img/f02n2c08.png b/benchmarks/img/f02n2c08.png
new file mode 100644
index 0000000..e590f12
Binary files /dev/null and b/benchmarks/img/f02n2c08.png differ
diff --git a/benchmarks/img/f03n0g08.png b/benchmarks/img/f03n0g08.png
new file mode 100644
index 0000000..ed01e29
Binary files /dev/null and b/benchmarks/img/f03n0g08.png differ
diff --git a/benchmarks/img/f03n2c08.png b/benchmarks/img/f03n2c08.png
new file mode 100644
index 0000000..7581150
Binary files /dev/null and b/benchmarks/img/f03n2c08.png differ
diff --git a/benchmarks/img/f04n0g08.png b/benchmarks/img/f04n0g08.png
new file mode 100644
index 0000000..663fdae
Binary files /dev/null and b/benchmarks/img/f04n0g08.png differ
diff --git a/benchmarks/img/f04n2c08.png b/benchmarks/img/f04n2c08.png
new file mode 100644
index 0000000..3c8b511
Binary files /dev/null and b/benchmarks/img/f04n2c08.png differ
diff --git a/benchmarks/img/f99n0g04.png b/benchmarks/img/f99n0g04.png
new file mode 100644
index 0000000..0b521c1
Binary files /dev/null and b/benchmarks/img/f99n0g04.png differ
diff --git a/benchmarks/img/g03n0g16.png b/benchmarks/img/g03n0g16.png
new file mode 100644
index 0000000..41083ca
Binary files /dev/null and b/benchmarks/img/g03n0g16.png differ
diff --git a/benchmarks/img/g03n2c08.png b/benchmarks/img/g03n2c08.png
new file mode 100644
index 0000000..a9354db
Binary files /dev/null and b/benchmarks/img/g03n2c08.png differ
diff --git a/benchmarks/img/g03n3p04.png b/benchmarks/img/g03n3p04.png
new file mode 100644
index 0000000..60396c9
Binary files /dev/null and b/benchmarks/img/g03n3p04.png differ
diff --git a/benchmarks/img/g04n0g16.png b/benchmarks/img/g04n0g16.png
new file mode 100644
index 0000000..32395b7
Binary files /dev/null and b/benchmarks/img/g04n0g16.png differ
diff --git a/benchmarks/img/g04n2c08.png b/benchmarks/img/g04n2c08.png
new file mode 100644
index 0000000..a652b0c
Binary files /dev/null and b/benchmarks/img/g04n2c08.png differ
diff --git a/benchmarks/img/g04n3p04.png b/benchmarks/img/g04n3p04.png
new file mode 100644
index 0000000..5661cc3
Binary files /dev/null and b/benchmarks/img/g04n3p04.png differ
diff --git a/benchmarks/img/g05n0g16.png b/benchmarks/img/g05n0g16.png
new file mode 100644
index 0000000..70b37f0
Binary files /dev/null and b/benchmarks/img/g05n0g16.png differ
diff --git a/benchmarks/img/g05n2c08.png b/benchmarks/img/g05n2c08.png
new file mode 100644
index 0000000..932c136
Binary files /dev/null and b/benchmarks/img/g05n2c08.png differ
diff --git a/benchmarks/img/g05n3p04.png b/benchmarks/img/g05n3p04.png
new file mode 100644
index 0000000..9619930
Binary files /dev/null and b/benchmarks/img/g05n3p04.png differ
diff --git a/benchmarks/img/g07n0g16.png b/benchmarks/img/g07n0g16.png
new file mode 100644
index 0000000..d6a47c2
Binary files /dev/null and b/benchmarks/img/g07n0g16.png differ
diff --git a/benchmarks/img/g07n2c08.png b/benchmarks/img/g07n2c08.png
new file mode 100644
index 0000000..5973464
Binary files /dev/null and b/benchmarks/img/g07n2c08.png differ
diff --git a/benchmarks/img/g07n3p04.png b/benchmarks/img/g07n3p04.png
new file mode 100644
index 0000000..c73fb61
Binary files /dev/null and b/benchmarks/img/g07n3p04.png differ
diff --git a/benchmarks/img/g10n0g16.png b/benchmarks/img/g10n0g16.png
new file mode 100644
index 0000000..85f2c95
Binary files /dev/null and b/benchmarks/img/g10n0g16.png differ
diff --git a/benchmarks/img/g10n2c08.png b/benchmarks/img/g10n2c08.png
new file mode 100644
index 0000000..b303997
Binary files /dev/null and b/benchmarks/img/g10n2c08.png differ
diff --git a/benchmarks/img/g10n3p04.png b/benchmarks/img/g10n3p04.png
new file mode 100644
index 0000000..1b6a6be
Binary files /dev/null and b/benchmarks/img/g10n3p04.png differ
diff --git a/benchmarks/img/g25n0g16.png b/benchmarks/img/g25n0g16.png
new file mode 100644
index 0000000..a9f6787
Binary files /dev/null and b/benchmarks/img/g25n0g16.png differ
diff --git a/benchmarks/img/g25n2c08.png b/benchmarks/img/g25n2c08.png
new file mode 100644
index 0000000..03f505a
Binary files /dev/null and b/benchmarks/img/g25n2c08.png differ
diff --git a/benchmarks/img/g25n3p04.png b/benchmarks/img/g25n3p04.png
new file mode 100644
index 0000000..4f943c6
Binary files /dev/null and b/benchmarks/img/g25n3p04.png differ
diff --git a/benchmarks/img/oi1n0g16.png b/benchmarks/img/oi1n0g16.png
new file mode 100644
index 0000000..e7c82f7
Binary files /dev/null and b/benchmarks/img/oi1n0g16.png differ
diff --git a/benchmarks/img/oi1n2c16.png b/benchmarks/img/oi1n2c16.png
new file mode 100644
index 0000000..50c1cb9
Binary files /dev/null and b/benchmarks/img/oi1n2c16.png differ
diff --git a/benchmarks/img/oi2n0g16.png b/benchmarks/img/oi2n0g16.png
new file mode 100644
index 0000000..14d64c5
Binary files /dev/null and b/benchmarks/img/oi2n0g16.png differ
diff --git a/benchmarks/img/oi2n2c16.png b/benchmarks/img/oi2n2c16.png
new file mode 100644
index 0000000..4c2e3e3
Binary files /dev/null and b/benchmarks/img/oi2n2c16.png differ
diff --git a/benchmarks/img/oi4n0g16.png b/benchmarks/img/oi4n0g16.png
new file mode 100644
index 0000000..69e73ed
Binary files /dev/null and b/benchmarks/img/oi4n0g16.png differ
diff --git a/benchmarks/img/oi4n2c16.png b/benchmarks/img/oi4n2c16.png
new file mode 100644
index 0000000..93691e3
Binary files /dev/null and b/benchmarks/img/oi4n2c16.png differ
diff --git a/benchmarks/img/oi9n0g16.png b/benchmarks/img/oi9n0g16.png
new file mode 100644
index 0000000..9248413
Binary files /dev/null and b/benchmarks/img/oi9n0g16.png differ
diff --git a/benchmarks/img/oi9n2c16.png b/benchmarks/img/oi9n2c16.png
new file mode 100644
index 0000000..f0512e4
Binary files /dev/null and b/benchmarks/img/oi9n2c16.png differ
diff --git a/benchmarks/img/pp0n2c16.png b/benchmarks/img/pp0n2c16.png
new file mode 100644
index 0000000..8f2aad7
Binary files /dev/null and b/benchmarks/img/pp0n2c16.png differ
diff --git a/benchmarks/img/pp0n6a08.png b/benchmarks/img/pp0n6a08.png
new file mode 100644
index 0000000..4ed7a30
Binary files /dev/null and b/benchmarks/img/pp0n6a08.png differ
diff --git a/benchmarks/img/ps1n0g08.png b/benchmarks/img/ps1n0g08.png
new file mode 100644
index 0000000..99625fa
Binary files /dev/null and b/benchmarks/img/ps1n0g08.png differ
diff --git a/benchmarks/img/ps1n2c16.png b/benchmarks/img/ps1n2c16.png
new file mode 100644
index 0000000..0c7a6b3
Binary files /dev/null and b/benchmarks/img/ps1n2c16.png differ
diff --git a/benchmarks/img/ps2n0g08.png b/benchmarks/img/ps2n0g08.png
new file mode 100644
index 0000000..90b2979
Binary files /dev/null and b/benchmarks/img/ps2n0g08.png differ
diff --git a/benchmarks/img/ps2n2c16.png b/benchmarks/img/ps2n2c16.png
new file mode 100644
index 0000000..a4a181e
Binary files /dev/null and b/benchmarks/img/ps2n2c16.png differ
diff --git a/benchmarks/img/s01i3p01.png b/benchmarks/img/s01i3p01.png
new file mode 100644
index 0000000..6c0fad1
Binary files /dev/null and b/benchmarks/img/s01i3p01.png differ
diff --git a/benchmarks/img/s01n3p01.png b/benchmarks/img/s01n3p01.png
new file mode 100644
index 0000000..cb2c8c7
Binary files /dev/null and b/benchmarks/img/s01n3p01.png differ
diff --git a/benchmarks/img/s02i3p01.png b/benchmarks/img/s02i3p01.png
new file mode 100644
index 0000000..2defaed
Binary files /dev/null and b/benchmarks/img/s02i3p01.png differ
diff --git a/benchmarks/img/s02n3p01.png b/benchmarks/img/s02n3p01.png
new file mode 100644
index 0000000..2b1b669
Binary files /dev/null and b/benchmarks/img/s02n3p01.png differ
diff --git a/benchmarks/img/s03i3p01.png b/benchmarks/img/s03i3p01.png
new file mode 100644
index 0000000..c23fdc4
Binary files /dev/null and b/benchmarks/img/s03i3p01.png differ
diff --git a/benchmarks/img/s03n3p01.png b/benchmarks/img/s03n3p01.png
new file mode 100644
index 0000000..6d96ee4
Binary files /dev/null and b/benchmarks/img/s03n3p01.png differ
diff --git a/benchmarks/img/s04i3p01.png b/benchmarks/img/s04i3p01.png
new file mode 100644
index 0000000..0e710c2
Binary files /dev/null and b/benchmarks/img/s04i3p01.png differ
diff --git a/benchmarks/img/s04n3p01.png b/benchmarks/img/s04n3p01.png
new file mode 100644
index 0000000..956396c
Binary files /dev/null and b/benchmarks/img/s04n3p01.png differ
diff --git a/benchmarks/img/s05i3p02.png b/benchmarks/img/s05i3p02.png
new file mode 100644
index 0000000..d14cbd3
Binary files /dev/null and b/benchmarks/img/s05i3p02.png differ
diff --git a/benchmarks/img/s05n3p02.png b/benchmarks/img/s05n3p02.png
new file mode 100644
index 0000000..bf940f0
Binary files /dev/null and b/benchmarks/img/s05n3p02.png differ
diff --git a/benchmarks/img/s06i3p02.png b/benchmarks/img/s06i3p02.png
new file mode 100644
index 0000000..456ada3
Binary files /dev/null and b/benchmarks/img/s06i3p02.png differ
diff --git a/benchmarks/img/s06n3p02.png b/benchmarks/img/s06n3p02.png
new file mode 100644
index 0000000..501064d
Binary files /dev/null and b/benchmarks/img/s06n3p02.png differ
diff --git a/benchmarks/img/s07i3p02.png b/benchmarks/img/s07i3p02.png
new file mode 100644
index 0000000..44b66ba
Binary files /dev/null and b/benchmarks/img/s07i3p02.png differ
diff --git a/benchmarks/img/s07n3p02.png b/benchmarks/img/s07n3p02.png
new file mode 100644
index 0000000..6a58259
Binary files /dev/null and b/benchmarks/img/s07n3p02.png differ
diff --git a/benchmarks/img/s08i3p02.png b/benchmarks/img/s08i3p02.png
new file mode 100644
index 0000000..acf74f3
Binary files /dev/null and b/benchmarks/img/s08i3p02.png differ
diff --git a/benchmarks/img/s08n3p02.png b/benchmarks/img/s08n3p02.png
new file mode 100644
index 0000000..b7094e1
Binary files /dev/null and b/benchmarks/img/s08n3p02.png differ
diff --git a/benchmarks/img/s09i3p02.png b/benchmarks/img/s09i3p02.png
new file mode 100644
index 0000000..0bfae8e
Binary files /dev/null and b/benchmarks/img/s09i3p02.png differ
diff --git a/benchmarks/img/s09n3p02.png b/benchmarks/img/s09n3p02.png
new file mode 100644
index 0000000..711ab82
Binary files /dev/null and b/benchmarks/img/s09n3p02.png differ
diff --git a/benchmarks/img/s32i3p04.png b/benchmarks/img/s32i3p04.png
new file mode 100644
index 0000000..0841910
Binary files /dev/null and b/benchmarks/img/s32i3p04.png differ
diff --git a/benchmarks/img/s32n3p04.png b/benchmarks/img/s32n3p04.png
new file mode 100644
index 0000000..fa58e3e
Binary files /dev/null and b/benchmarks/img/s32n3p04.png differ
diff --git a/benchmarks/img/s33i3p04.png b/benchmarks/img/s33i3p04.png
new file mode 100644
index 0000000..ab0dc14
Binary files /dev/null and b/benchmarks/img/s33i3p04.png differ
diff --git a/benchmarks/img/s33n3p04.png b/benchmarks/img/s33n3p04.png
new file mode 100644
index 0000000..764f1a3
Binary files /dev/null and b/benchmarks/img/s33n3p04.png differ
diff --git a/benchmarks/img/s34i3p04.png b/benchmarks/img/s34i3p04.png
new file mode 100644
index 0000000..bd99039
Binary files /dev/null and b/benchmarks/img/s34i3p04.png differ
diff --git a/benchmarks/img/s34n3p04.png b/benchmarks/img/s34n3p04.png
new file mode 100644
index 0000000..9cbc68b
Binary files /dev/null and b/benchmarks/img/s34n3p04.png differ
diff --git a/benchmarks/img/s35i3p04.png b/benchmarks/img/s35i3p04.png
new file mode 100644
index 0000000..e2a5e0a
Binary files /dev/null and b/benchmarks/img/s35i3p04.png differ
diff --git a/benchmarks/img/s35n3p04.png b/benchmarks/img/s35n3p04.png
new file mode 100644
index 0000000..90b892e
Binary files /dev/null and b/benchmarks/img/s35n3p04.png differ
diff --git a/benchmarks/img/s36i3p04.png b/benchmarks/img/s36i3p04.png
new file mode 100644
index 0000000..eb61b6f
Binary files /dev/null and b/benchmarks/img/s36i3p04.png differ
diff --git a/benchmarks/img/s36n3p04.png b/benchmarks/img/s36n3p04.png
new file mode 100644
index 0000000..b38d179
Binary files /dev/null and b/benchmarks/img/s36n3p04.png differ
diff --git a/benchmarks/img/s37i3p04.png b/benchmarks/img/s37i3p04.png
new file mode 100644
index 0000000..6e2b1e9
Binary files /dev/null and b/benchmarks/img/s37i3p04.png differ
diff --git a/benchmarks/img/s37n3p04.png b/benchmarks/img/s37n3p04.png
new file mode 100644
index 0000000..4d3054d
Binary files /dev/null and b/benchmarks/img/s37n3p04.png differ
diff --git a/benchmarks/img/s38i3p04.png b/benchmarks/img/s38i3p04.png
new file mode 100644
index 0000000..a0a8a14
Binary files /dev/null and b/benchmarks/img/s38i3p04.png differ
diff --git a/benchmarks/img/s38n3p04.png b/benchmarks/img/s38n3p04.png
new file mode 100644
index 0000000..1233ed0
Binary files /dev/null and b/benchmarks/img/s38n3p04.png differ
diff --git a/benchmarks/img/s39i3p04.png b/benchmarks/img/s39i3p04.png
new file mode 100644
index 0000000..04fee93
Binary files /dev/null and b/benchmarks/img/s39i3p04.png differ
diff --git a/benchmarks/img/s39n3p04.png b/benchmarks/img/s39n3p04.png
new file mode 100644
index 0000000..c750100
Binary files /dev/null and b/benchmarks/img/s39n3p04.png differ
diff --git a/benchmarks/img/s40i3p04.png b/benchmarks/img/s40i3p04.png
new file mode 100644
index 0000000..68f358b
Binary files /dev/null and b/benchmarks/img/s40i3p04.png differ
diff --git a/benchmarks/img/s40n3p04.png b/benchmarks/img/s40n3p04.png
new file mode 100644
index 0000000..864b6b9
Binary files /dev/null and b/benchmarks/img/s40n3p04.png differ
diff --git a/benchmarks/img/tbbn0g04.png b/benchmarks/img/tbbn0g04.png
new file mode 100644
index 0000000..39a7050
Binary files /dev/null and b/benchmarks/img/tbbn0g04.png differ
diff --git a/benchmarks/img/tbbn2c16.png b/benchmarks/img/tbbn2c16.png
new file mode 100644
index 0000000..dd3168e
Binary files /dev/null and b/benchmarks/img/tbbn2c16.png differ
diff --git a/benchmarks/img/tbbn3p08.png b/benchmarks/img/tbbn3p08.png
new file mode 100644
index 0000000..0ede357
Binary files /dev/null and b/benchmarks/img/tbbn3p08.png differ
diff --git a/benchmarks/img/tbgn2c16.png b/benchmarks/img/tbgn2c16.png
new file mode 100644
index 0000000..85cec39
Binary files /dev/null and b/benchmarks/img/tbgn2c16.png differ
diff --git a/benchmarks/img/tbgn3p08.png b/benchmarks/img/tbgn3p08.png
new file mode 100644
index 0000000..8cf2e6f
Binary files /dev/null and b/benchmarks/img/tbgn3p08.png differ
diff --git a/benchmarks/img/tbrn2c08.png b/benchmarks/img/tbrn2c08.png
new file mode 100644
index 0000000..5cca0d6
Binary files /dev/null and b/benchmarks/img/tbrn2c08.png differ
diff --git a/benchmarks/img/tbwn0g16.png b/benchmarks/img/tbwn0g16.png
new file mode 100644
index 0000000..99bdeed
Binary files /dev/null and b/benchmarks/img/tbwn0g16.png differ
diff --git a/benchmarks/img/tbwn3p08.png b/benchmarks/img/tbwn3p08.png
new file mode 100644
index 0000000..eacab7a
Binary files /dev/null and b/benchmarks/img/tbwn3p08.png differ
diff --git a/benchmarks/img/tbyn3p08.png b/benchmarks/img/tbyn3p08.png
new file mode 100644
index 0000000..656db09
Binary files /dev/null and b/benchmarks/img/tbyn3p08.png differ
diff --git a/benchmarks/img/tm3n3p02.png b/benchmarks/img/tm3n3p02.png
new file mode 100644
index 0000000..fb3ef1d
Binary files /dev/null and b/benchmarks/img/tm3n3p02.png differ
diff --git a/benchmarks/img/tp0n0g08.png b/benchmarks/img/tp0n0g08.png
new file mode 100644
index 0000000..333465f
Binary files /dev/null and b/benchmarks/img/tp0n0g08.png differ
diff --git a/benchmarks/img/tp0n2c08.png b/benchmarks/img/tp0n2c08.png
new file mode 100644
index 0000000..fc6e42c
Binary files /dev/null and b/benchmarks/img/tp0n2c08.png differ
diff --git a/benchmarks/img/tp0n3p08.png b/benchmarks/img/tp0n3p08.png
new file mode 100644
index 0000000..69a69e5
Binary files /dev/null and b/benchmarks/img/tp0n3p08.png differ
diff --git a/benchmarks/img/tp1n3p08.png b/benchmarks/img/tp1n3p08.png
new file mode 100644
index 0000000..a6c9f35
Binary files /dev/null and b/benchmarks/img/tp1n3p08.png differ
diff --git a/benchmarks/img/z00n2c08.png b/benchmarks/img/z00n2c08.png
new file mode 100644
index 0000000..7669eb8
Binary files /dev/null and b/benchmarks/img/z00n2c08.png differ
diff --git a/benchmarks/img/z03n2c08.png b/benchmarks/img/z03n2c08.png
new file mode 100644
index 0000000..bfb10de
Binary files /dev/null and b/benchmarks/img/z03n2c08.png differ
diff --git a/benchmarks/img/z06n2c08.png b/benchmarks/img/z06n2c08.png
new file mode 100644
index 0000000..b90ebc1
Binary files /dev/null and b/benchmarks/img/z06n2c08.png differ
diff --git a/benchmarks/img/z09n2c08.png b/benchmarks/img/z09n2c08.png
new file mode 100644
index 0000000..5f191a7
Binary files /dev/null and b/benchmarks/img/z09n2c08.png differ
diff --git a/bin/Crunch.app/Contents/Info.plist b/bin/Crunch.app/Contents/Info.plist
index 903a3f5..33a84e1 100644
Binary files a/bin/Crunch.app/Contents/Info.plist and b/bin/Crunch.app/Contents/Info.plist differ
diff --git a/bin/Crunch.app/Contents/MacOS/Crunch b/bin/Crunch.app/Contents/MacOS/Crunch
index d4f4459..249a99b 100755
Binary files a/bin/Crunch.app/Contents/MacOS/Crunch and b/bin/Crunch.app/Contents/MacOS/Crunch differ
diff --git a/bin/Crunch.app/Contents/Resources/AppSettings.plist b/bin/Crunch.app/Contents/Resources/AppSettings.plist
index 2798294..3e068fb 100644
Binary files a/bin/Crunch.app/Contents/Resources/AppSettings.plist and b/bin/Crunch.app/Contents/Resources/AppSettings.plist differ
diff --git a/bin/Crunch.app/Contents/Resources/appIcon.icns b/bin/Crunch.app/Contents/Resources/appIcon.icns
index acae9e0..f8cb623 100644
Binary files a/bin/Crunch.app/Contents/Resources/appIcon.icns and b/bin/Crunch.app/Contents/Resources/appIcon.icns differ
diff --git a/bin/Crunch.app/Contents/Resources/crunch.py b/bin/Crunch.app/Contents/Resources/crunch.py
index d3d1944..71a4b59 100755
--- a/bin/Crunch.app/Contents/Resources/crunch.py
+++ b/bin/Crunch.app/Contents/Resources/crunch.py
@@ -5,7 +5,7 @@
# crunch
# A PNG file optimization tool built on pngquant and zopflipng
#
-# Copyright 2018 Christopher Simpkins
+# Copyright 2019 Christopher Simpkins
# MIT License
#
# Source Repository: https://github.com/chrissimpkins/Crunch
@@ -25,6 +25,10 @@
stdstream_lock = Lock()
logging_lock = Lock()
+# Application Constants
+VERSION = "4.0.0"
+VERSION_STRING = "crunch v" + VERSION
+
# Processor Constant
# - Modify this to an integer value if you want to fix the number of
# processes spawned during execution. The process number is
@@ -44,18 +48,14 @@
# Log File Path Constants
LOGFILE_PATH = os.path.join(CRUNCH_DOT_DIRECTORY, "crunch.log")
-# Application Constants
-VERSION = "3.0.1"
-VERSION_STRING = "crunch v" + VERSION
-
HELP_STRING = """
-///////////////////////////////////////////////////
+==================================================
crunch
- Copyright 2018 Christopher Simpkins
+ Copyright 2019 Christopher Simpkins
MIT License
Source: https://github.com/chrissimpkins/Crunch
-///////////////////////////////////////////////////
+==================================================
crunch is a command line executable that performs lossy optimization of one or more png image files with pngquant and zopflipng.
@@ -70,25 +70,33 @@
USAGE = "$ crunch [image path 1]...[image path n]"
-# Create the Crunch dot directory in $HOME if it does not exist
-# Only used for macOS GUI and macOS right-click menu service execution
-if sys.argv[1] in ("--gui", "--service"):
- if not os.path.isdir(CRUNCH_DOT_DIRECTORY):
- os.makedirs(CRUNCH_DOT_DIRECTORY)
- # clear the text in the log file before every script execution
- # logging is only maintained for the last execution of the script
- open(LOGFILE_PATH, "w").close()
-
def main(argv):
+ # Create the Crunch dot directory in $HOME if it does not exist
+ # Only used for macOS GUI and macOS right-click menu service execution
+ if len(argv) > 0 and argv[0] in ("--gui", "--service"):
+ if not os.path.isdir(CRUNCH_DOT_DIRECTORY):
+ os.makedirs(CRUNCH_DOT_DIRECTORY)
+ # clear the text in the log file before every script execution
+ # logging is only maintained for the last execution of the script
+ open(LOGFILE_PATH, "w").close()
+
+ # ////////////////////////
+ # ANSI COLOR DEFINITIONS
+ # ////////////////////////
+ if not is_gui(sys.argv):
+ ERROR_STRING = "[ " + format_ansi_red("!") + " ]"
+ else:
+ ERROR_STRING = "[ ! ]"
+
# //////////////////////////////////
# CONFIRM ARGUMENT PRESENT
# //////////////////////////////////
if len(argv) == 0:
sys.stderr.write(
- "[ERROR] Please include one or more paths to PNG image files as "
+ ERROR_STRING + " Please include one or more paths to PNG image files as "
"arguments to the script." + os.linesep
)
sys.exit(1)
@@ -130,42 +138,65 @@ def main(argv):
for png_path in png_path_list:
# Not a file test
if not os.path.isfile(png_path): # is not an existing file
- sys.stderr.write("[ERROR] '" + png_path + "' does not appear to be a valid path to a PNG file" + os.linesep)
+ sys.stderr.write(
+ ERROR_STRING
+ + " '"
+ + png_path
+ + "' does not appear to be a valid path to a PNG file"
+ + os.linesep
+ )
sys.exit(1) # not a file, abort immediately
# PNG validity test
if not is_valid_png(png_path):
- sys.stderr.write("[ERROR] '" + png_path + "' is not a valid PNG file." + os.linesep)
+ sys.stderr.write(
+ ERROR_STRING
+ + " '"
+ + png_path
+ + "' is not a valid PNG file."
+ + os.linesep
+ )
if is_gui(argv):
log_error(png_path + " is not a valid PNG file.")
NOTPNG_ERROR_FOUND = True
# Exit after checking all file requests and reporting on all invalid file paths (above)
if NOTPNG_ERROR_FOUND is True:
- sys.stderr.write("The request was not executed successfully. Please try again with one or more valid PNG files." + os.linesep)
+ sys.stderr.write(
+ "The request was not executed successfully. Please try again with one or more valid PNG files."
+ + os.linesep
+ )
if is_gui(argv):
- log_error("The request was not executed successfully. Please try again with one or more valid PNG files.")
+ log_error(
+ "The request was not executed successfully. Please try again with one or more valid PNG files."
+ )
sys.exit(1)
# Dependency error handling
if not os.path.exists(PNGQUANT_EXE_PATH):
sys.stderr.write(
- "[ERROR] pngquant executable was not identified on path '"
+ ERROR_STRING
+ + " pngquant executable was not identified on path '"
+ PNGQUANT_EXE_PATH
+ "'"
+ os.linesep
)
if is_gui(argv):
- log_error("pngquant was not found on the expected path " + PNGQUANT_EXE_PATH)
+ log_error(
+ "pngquant was not found on the expected path " + PNGQUANT_EXE_PATH
+ )
sys.exit(1)
elif not os.path.exists(ZOPFLIPNG_EXE_PATH):
sys.stderr.write(
- "[ERROR] zopflipng executable was not identified on path '"
+ ERROR_STRING
+ + " zopflipng executable was not identified on path '"
+ ZOPFLIPNG_EXE_PATH
+ "'"
+ os.linesep
)
if is_gui(argv):
- log_error("zopflipng was not found on the expected path " + ZOPFLIPNG_EXE_PATH)
+ log_error(
+ "zopflipng was not found on the expected path " + ZOPFLIPNG_EXE_PATH
+ )
sys.exit(1)
# ////////////////////////////////////
@@ -186,14 +217,24 @@ def main(argv):
if processes > len(png_path_list):
processes = len(png_path_list)
- print("Spawning " + str(processes) + " processes to optimize " + str(len(png_path_list)) + " image files...")
+ print(
+ "Spawning "
+ + str(processes)
+ + " processes to optimize "
+ + str(len(png_path_list))
+ + " image files..."
+ )
p = Pool(processes)
try:
p.map(optimize_png, png_path_list)
except Exception as e:
stdstream_lock.acquire()
sys.stderr.write("-----" + os.linesep)
- sys.stderr.write("[ERROR] Error detected during execution of the request." + os.linesep)
+ sys.stderr.write(
+ ERROR_STRING
+ + " Error detected during execution of the request."
+ + os.linesep
+ )
sys.stderr.write(str(e) + os.linesep)
stdstream_lock.release()
if is_gui(argv):
@@ -213,15 +254,28 @@ def main(argv):
def optimize_png(png_path):
img = ImageFile(png_path)
+
# define pngquant and zopflipng paths
PNGQUANT_EXE_PATH = get_pngquant_path()
ZOPFLIPNG_EXE_PATH = get_zopflipng_path()
+ # ////////////////////////
+ # ANSI COLOR DEFINITIONS
+ # ////////////////////////
+ if not is_gui(sys.argv):
+ ERROR_STRING = "[ " + format_ansi_red("!") + " ]"
+ else:
+ ERROR_STRING = "[ ! ]"
+
# --------------
# pngquant stage
# --------------
- pngquant_options = " --quality=80-98 --skip-if-larger --force --strip --speed 1 --ext -crunch.png "
- pngquant_command = PNGQUANT_EXE_PATH + pngquant_options + shellquote(img.pre_filepath)
+ pngquant_options = (
+ " --quality=80-98 --skip-if-larger --force --strip --speed 1 --ext -crunch.png "
+ )
+ pngquant_command = (
+ PNGQUANT_EXE_PATH + pngquant_options + shellquote(img.pre_filepath)
+ )
try:
subprocess.check_output(pngquant_command, stderr=subprocess.STDOUT, shell=True)
except CalledProcessError as cpe:
@@ -237,16 +291,32 @@ def optimize_png(png_path):
pass
else:
stdstream_lock.acquire()
- sys.stderr.write("[ERROR] " + img.pre_filepath + " processing failed at the pngquant stage." + os.linesep)
+ sys.stderr.write(
+ ERROR_STRING
+ + " "
+ + img.pre_filepath
+ + " processing failed at the pngquant stage."
+ + os.linesep
+ )
stdstream_lock.release()
if is_gui(sys.argv):
- log_error(img.pre_filepath + " processing failed at the pngquant stage. " + os.linesep + str(cpe))
+ log_error(
+ img.pre_filepath
+ + " processing failed at the pngquant stage. "
+ + os.linesep
+ + str(cpe)
+ )
return None
else:
raise cpe
except Exception as e:
if is_gui(sys.argv):
- log_error(img.pre_filepath + " processing failed at the pngquant stage. " + os.linesep + str(e))
+ log_error(
+ img.pre_filepath
+ + " processing failed at the pngquant stage. "
+ + os.linesep
+ + str(e)
+ )
return None
else:
raise e
@@ -264,21 +334,43 @@ def optimize_png(png_path):
# filters. This achieves better compression than the default approach for non-quantized PNG
# files, but takes significantly longer (based upon testing by CS)
zopflipng_options = " -y --lossy_transparent "
- zopflipng_command = ZOPFLIPNG_EXE_PATH + zopflipng_options + shellquote(img.post_filepath) + " " + shellquote(img.post_filepath)
+ zopflipng_command = (
+ ZOPFLIPNG_EXE_PATH
+ + zopflipng_options
+ + shellquote(img.post_filepath)
+ + " "
+ + shellquote(img.post_filepath)
+ )
try:
subprocess.check_output(zopflipng_command, stderr=subprocess.STDOUT, shell=True)
except CalledProcessError as cpe:
stdstream_lock.acquire()
- sys.stderr.write("[ERROR] " + img.pre_filepath + " processing failed at the zopflipng stage." + os.linesep)
+ sys.stderr.write(
+ ERROR_STRING
+ + " "
+ + img.pre_filepath
+ + " processing failed at the zopflipng stage."
+ + os.linesep
+ )
stdstream_lock.release()
if is_gui(sys.argv):
- log_error(img.pre_filepath + " processing failed at the zopflipng stage. " + os.linesep + str(cpe))
+ log_error(
+ img.pre_filepath
+ + " processing failed at the zopflipng stage. "
+ + os.linesep
+ + str(cpe)
+ )
return None
else:
raise cpe
except Exception as e:
if is_gui(sys.argv):
- log_error(img.pre_filepath + " processing failed at the pngquant stage. " + os.linesep + str(e))
+ log_error(
+ img.pre_filepath
+ + " processing failed at the pngquant stage. "
+ + os.linesep
+ + str(e)
+ )
return None
else:
raise e
@@ -286,17 +378,41 @@ def optimize_png(png_path):
# Check file size post-optimization and report comparison with pre-optimization file
img.get_post_filesize()
percent = img.get_compression_percent()
- percent_string = '{0:.2f}'.format(percent)
+ percent_string = "{0:.2f}%".format(percent)
+ # if compression occurred, color the percent string green
+ # otherwise, leave it default text color
+ if not is_gui(sys.argv) and percent < 100:
+ percent_string = format_ansi_green(percent_string)
# report percent original file size / post file path / size (bytes) to stdout (command line executable)
stdstream_lock.acquire()
- print("[ " + percent_string + "% ] " + img.post_filepath + " (" + str(img.post_size) + " bytes)")
+ print(
+ "[ "
+ + percent_string
+ + " ] "
+ + img.post_filepath
+ + " ("
+ + str(img.post_size)
+ + " bytes)"
+ )
stdstream_lock.release()
- # report percent original file size / post file path / size (bytes) to stdout (macOS GUI + right-click service)
+ # report percent original file size / post file path / size (bytes) to log file (macOS GUI + right-click service)
if is_gui(sys.argv):
- log_info("[ " + percent_string + "% ] " +
- img.post_filepath + " (" + str(img.post_size) + " bytes)")
+ log_info(
+ "[ "
+ + percent_string
+ + " ] "
+ + img.post_filepath
+ + " ("
+ + str(img.post_size)
+ + " bytes)"
+ )
+
+
+# -----------
+# Utilities
+# -----------
def fix_filepath_args(args):
@@ -342,14 +458,14 @@ def get_zopflipng_path():
def is_gui(arglist):
- return ("--gui" in arglist or "--service" in arglist)
+ return "--gui" in arglist or "--service" in arglist
def is_valid_png(filepath):
# The PNG byte signature (https://www.w3.org/TR/PNG/#5PNG-file-signature)
- expected_signature = struct.pack('8B', 137, 80, 78, 71, 13, 10, 26, 10)
+ expected_signature = struct.pack("8B", 137, 80, 78, 71, 13, 10, 26, 10)
# open the file and read first 8 bytes
- with open(filepath, 'rb') as filer:
+ with open(filepath, "rb") as filer:
signature = filer.read(8)
# return boolean test result for first eight bytes == expected PNG byte signature
return signature == expected_signature
@@ -358,7 +474,7 @@ def is_valid_png(filepath):
def log_error(errmsg):
current_time = time.strftime("%m-%d-%y %H:%M:%S")
logging_lock.acquire()
- with open(LOGFILE_PATH, 'a') as filewriter:
+ with open(LOGFILE_PATH, "a") as filewriter:
filewriter.write(current_time + "\tERROR\t" + errmsg + os.linesep)
filewriter.flush()
os.fsync(filewriter.fileno())
@@ -368,7 +484,7 @@ def log_error(errmsg):
def log_info(infomsg):
current_time = time.strftime("%m-%d-%y %H:%M:%S")
logging_lock.acquire()
- with open(LOGFILE_PATH, 'a') as filewriter:
+ with open(LOGFILE_PATH, "a") as filewriter:
filewriter.write(current_time + "\tINFO\t" + infomsg + os.linesep)
filewriter.flush()
os.fsync(filewriter.fileno())
@@ -380,6 +496,20 @@ def shellquote(filepath):
return "'" + filepath.replace("'", "'\\''") + "'"
+def format_ansi_red(text):
+ if sys.stdout.isatty():
+ return "\033[0;31m" + text + "\033[0m"
+ else:
+ return text
+
+
+def format_ansi_green(text):
+ if sys.stdout.isatty():
+ return "\033[0;32m" + text + "\033[0m"
+ else:
+ return text
+
+
# ///////////////////////
# OBJECT DEFINITIONS
# ///////////////////////
@@ -415,7 +545,7 @@ def get_compression_percent(self):
# This workaround reconstructs the original filepaths
# that are split by the shell script into separate arguments
# when there are spaces in the macOS file path
- if sys.argv[1] in ("--gui", "--service"):
+ if len(sys.argv) > 1 and sys.argv[1] in ("--gui", "--service"):
arg_list = fix_filepath_args(sys.argv[1:])
main(arg_list)
else:
diff --git a/bin/Crunch.app/Contents/Resources/pngquant b/bin/Crunch.app/Contents/Resources/pngquant
index d8db8d2..5f68d8d 100755
Binary files a/bin/Crunch.app/Contents/Resources/pngquant and b/bin/Crunch.app/Contents/Resources/pngquant differ
diff --git a/bin/Crunch.app/Contents/Resources/script b/bin/Crunch.app/Contents/Resources/script
index 59687c3..2099805 100755
--- a/bin/Crunch.app/Contents/Resources/script
+++ b/bin/Crunch.app/Contents/Resources/script
@@ -15,6 +15,10 @@
#
# ///////////////////////////////////////////////////////////
+# UNCOMMENT FOR TESTING ONLY
+# python crunch.py --gui "$@"
+# exit 0
+
# Message on application open (no arguments passed to script on initial open)
if [ $# -eq 0 ]; then
cat waiting.html
diff --git a/bin/Crunch.app/Contents/Resources/zopflipng b/bin/Crunch.app/Contents/Resources/zopflipng
index f1caf2e..ac750b9 100755
Binary files a/bin/Crunch.app/Contents/Resources/zopflipng and b/bin/Crunch.app/Contents/Resources/zopflipng differ
diff --git a/dmg-builder.sh b/dmg-builder.sh
index 324f917..ffbb5e2 100755
--- a/dmg-builder.sh
+++ b/dmg-builder.sh
@@ -1,8 +1,10 @@
#!/bin/sh
-/Users/ces/Desktop/extcode/create-dmg/create-dmg \
+# This script uses the `create-dmg` script from https://github.com/andreyvit/create-dmg
+
+create-dmg \
--volname "Crunch Installer" \
---volicon "/Users/ces/Library/Application Support/Platypus/PlatypusIcon-17084.icns" \
+--volicon "/Users/chris/Library/Application Support/Platypus/PlatypusIcon-13299.icns" \
--background "img/dmg-installer-bg.png" \
--window-pos 200 120 \
--window-size 800 400 \
@@ -11,7 +13,7 @@
--hide-extension Crunch.app \
--app-drop-link 600 185 \
Crunch-Installer.dmg \
-/Users/ces/Desktop/code/Crunch/bin
+/Users/chris/code/Crunch/bin
# create checksum file for the installer
mv Crunch-Installer.dmg installer/Crunch-Installer.dmg
diff --git a/docs/BENCHMARKS.md b/docs/BENCHMARKS.md
index 557ae4b..8d25a6a 100644
--- a/docs/BENCHMARKS.md
+++ b/docs/BENCHMARKS.md
@@ -1,5 +1,7 @@
## Crunch Benchmarks
+**This is of historical interest only. The versions of tools in this comparison are outdated (including Crunch) and these should no longer be considered valid comparisons of current era tools. Please see the benchmarks directory of the repository for updated benchmark results.**
+
PNG optimization benchmarks were executed with Crunch v0.9.0. They use [the PNG Test Corpus files](http://css-ig.net/images/png-test-corpus.zip) that are maintained by Cédric Louvrier on css-ig.net. A broad analysis of available PNG optimization tools is available [here](http://css-ig.net/png-tools-overview) for additional reference.
In the following table, `Size In` and `Size Out` represent the original and optimized file size in bytes, respectively. `%Original Size` is the percent of the original file size in bytes represented by the final optimized file size. A lower value is better.
diff --git a/installer/Crunch-Installer-checksum.txt b/installer/Crunch-Installer-checksum.txt
index b332dbc..0549e42 100644
--- a/installer/Crunch-Installer-checksum.txt
+++ b/installer/Crunch-Installer-checksum.txt
@@ -1 +1 @@
-d532eb041f38904ce54f1aa638d9b7e8199fe729 Crunch-Installer.dmg
+269069da43847ac18487c9a527f83292587c6a08 Crunch-Installer.dmg
diff --git a/installer/Crunch-Installer.dmg b/installer/Crunch-Installer.dmg
index 3630861..e720088 100644
Binary files a/installer/Crunch-Installer.dmg and b/installer/Crunch-Installer.dmg differ
diff --git a/profile/Crunch.platypus b/profile/Crunch.platypus
index e9b0d8f..f57664e 100644
--- a/profile/Crunch.platypus
+++ b/profile/Crunch.platypus
@@ -12,29 +12,29 @@
Christopher Simpkins
BundledFiles
- /Users/ces/Desktop/code/Crunch/docs/Credits.html
- /Users/ces/Desktop/code/Crunch/html/execution.html
- /Users/ces/Desktop/code/Crunch/html/start.html
- /Users/ces/Desktop/code/Crunch/src/include/pngquant
- /Users/ces/Desktop/code/Crunch/src/include/zopflipng
- /Users/ces/Desktop/code/Crunch/html/clear.html
- /Users/ces/Desktop/code/Crunch/ui/MainMenu.nib
- /Users/ces/Desktop/code/Crunch/src/crunch.py
- /Users/ces/Desktop/code/Crunch/img/animations/01-Load-Up@2x.gif
- /Users/ces/Desktop/code/Crunch/img/animations/02-Waiting@2x.gif
- /Users/ces/Desktop/code/Crunch/img/animations/03-Crunching@2x.gif
- /Users/ces/Desktop/code/Crunch/img/animations/04-Error@2x.gif
- /Users/ces/Desktop/code/Crunch/img/animations/04-Success@2x.gif
- /Users/ces/Desktop/code/Crunch/html/waiting.html
- /Users/ces/Desktop/code/Crunch/html/complete-error.html
- /Users/ces/Desktop/code/Crunch/html/complete-success.html
+ /Users/chris/code/Crunch/docs/Credits.html
+ /Users/chris/code/Crunch/html/execution.html
+ /Users/chris/code/Crunch/html/start.html
+ /Users/chris/code/Crunch/src/include/pngquant
+ /Users/chris/code/Crunch/src/include/zopflipng
+ /Users/chris/code/Crunch/html/clear.html
+ /Users/chris/code/Crunch/ui/MainMenu.nib
+ /Users/chris/code/Crunch/src/crunch.py
+ /Users/chris/code/Crunch/img/animations/01-Load-Up@2x.gif
+ /Users/chris/code/Crunch/img/animations/02-Waiting@2x.gif
+ /Users/chris/code/Crunch/img/animations/03-Crunching@2x.gif
+ /Users/chris/code/Crunch/img/animations/04-Error@2x.gif
+ /Users/chris/code/Crunch/img/animations/04-Success@2x.gif
+ /Users/chris/code/Crunch/html/waiting.html
+ /Users/chris/code/Crunch/html/complete-error.html
+ /Users/chris/code/Crunch/html/complete-success.html
Creator
- Platypus-5.2
+ Platypus-5.3
DeclareService
Destination
- /Users/ces/Desktop/Application.app
+ /Users/chris/Desktop/Application.app
DevelopmentVersion
DocIconPath
@@ -44,7 +44,7 @@
ExecutablePath
/usr/local/share/platypus/ScriptExec
IconPath
- /Users/ces/Library/Application Support/Platypus/PlatypusIcon-91951.icns
+ /Users/chris/Library/Application Support/Platypus/PlatypusIcon-13299.icns
Identifier
com.csimpkins.Crunch
InterfaceType
@@ -70,179 +70,179 @@
ScriptArgs
ScriptPath
- /Users/ces/Desktop/code/Crunch/src/crunch-gui.sh
+ /Users/chris/code/Crunch/src/crunch-gui.sh
StatusItemDisplayType
Text
StatusItemIcon
- TU0AKgAAFEj///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////
- AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP//
- /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wAA
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIwAAACuAAAAAAAAAAAAAAAA
+ TU0AKgAADygAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAogAAAP8AAADOAAAAAAAAAAAAAAAAAAAAAQAAABAA
- AAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
- UgAAAB8AAAA5AAAA/wAAAN8AAAAAAAAAPgAAAKcAAADNAAAAzgMBAMUCAADJAAAAzwAA
- ALoAAABrAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASAAAAAAAAAAAAAAACAAAA/wAAAMEAAACV
- AAAA/wAAAK8AAAC0AgEA+B0EAsQ4BAN1OgQCQBoLCCcZBQQ1JAgEYicFAqgNAwHrAAAA
- 9gAAAIYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
- AAAAAAAAAAAAAAAAAAAAAADmAAAA0AAAAEAAAAAAAAAA8AAAAP8AAADhAAAA/wAAAP8j
- DAWvPAsFIE0HAwBgBQUATAMCABkNCgAaBAUANQsGAFIKBQBJDQYAPwoEcA8EAP0AAADc
- AAAAMwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
- AAAAAAAAAABgAAAA/wAAAP8AAACkAAAA1AAAAO4AAAD/CQMA40IZCDxVIA0ANAADADoB
- AgBFCAQAPQcDAB8JBQAbBwMALAoFAEIFAQBKCwUAexAHAFsaDAUQBgTSAAAA/wAAANsA
- AABKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
- 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
+ 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
+ AP8A/wD/AP8A/wD/AP8A4QCAAB0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAfwC9AN8A9QD7APsA9gDgAL4AgwAu
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHkzEw10
- lCsbAAwAAIgAAAD/AgEE8gAAAtEMCAqcAAAB9QAAAf8AAAD/AAAAJwAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAuFREAjDAjAEMtGQAb
- GxBsAAAA/wAABt8DBgzXAAAA/wAAAN0AAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAuEw8AhywfAEs3IABBPCUAGRQNOAsJ
- Bb0HAgLQLyILkEo+HQgAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/
- //8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A
- ////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////
- AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wAADgEAAAMAAAAB
- ACQAAAEBAAMAAAABACQAAAECAAMAAAAEAAAU9gEDAAMAAAABAAEAAAEGAAMAAAABAAIA
- AAERAAQAAAABAAAACAESAAMAAAABAAEAAAEVAAMAAAABAAQAAAEWAAMAAAABACQAAAEX
- AAQAAAABAAAUQAEcAAMAAAABAAEAAAFSAAMAAAABAAIAAAFTAAMAAAAEAAAU/odzAAcA
- AAxIAAAVBgAAAAAACAAIAAgACAABAAEAAQABAAAMSExpbm8CEAAAbW50clJHQiBYWVog
- B84AAgAJAAYAMQAAYWNzcE1TRlQAAAAASUVDIHNSR0IAAAAAAAAAAAAAAAAAAPbWAAEA
- AAAA0y1IUCAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
- AAAAAAARY3BydAAAAVAAAAAzZGVzYwAAAYQAAABsd3RwdAAAAfAAAAAUYmtwdAAAAgQA
- AAAUclhZWgAAAhgAAAAUZ1hZWgAAAiwAAAAUYlhZWgAAAkAAAAAUZG1uZAAAAlQAAABw
- ZG1kZAAAAsQAAACIdnVlZAAAA0wAAACGdmlldwAAA9QAAAAkbHVtaQAAA/gAAAAUbWVh
- cwAABAwAAAAkdGVjaAAABDAAAAAMclRSQwAABDwAAAgMZ1RSQwAABDwAAAgMYlRSQwAA
- BDwAAAgMdGV4dAAAAABDb3B5cmlnaHQgKGMpIDE5OTggSGV3bGV0dC1QYWNrYXJkIENv
- bXBhbnkAAGRlc2MAAAAAAAAAEnNSR0IgSUVDNjE5NjYtMi4xAAAAAAAAAAAAAAASc1JH
- QiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
- AAAAAAAAAAAAAAAAAFhZWiAAAAAAAADzUQABAAAAARbMWFlaIAAAAAAAAAAAAAAAAAAA
- AABYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAA
- ACSgAAAPhAAAts9kZXNjAAAAAAAAABZJRUMgaHR0cDovL3d3dy5pZWMuY2gAAAAAAAAA
- AAAAABZJRUMgaHR0cDovL3d3dy5pZWMuY2gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
- AAAAAAAAAAAAAAAAAAAAAAAAAAAAZGVzYwAAAAAAAAAuSUVDIDYxOTY2LTIuMSBEZWZh
- dWx0IFJHQiBjb2xvdXIgc3BhY2UgLSBzUkdCAAAAAAAAAAAAAAAuSUVDIDYxOTY2LTIu
- MSBEZWZhdWx0IFJHQiBjb2xvdXIgc3BhY2UgLSBzUkdCAAAAAAAAAAAAAAAAAAAAAAAA
- AAAAAGRlc2MAAAAAAAAALFJlZmVyZW5jZSBWaWV3aW5nIENvbmRpdGlvbiBpbiBJRUM2
- MTk2Ni0yLjEAAAAAAAAAAAAAACxSZWZlcmVuY2UgVmlld2luZyBDb25kaXRpb24gaW4g
- SUVDNjE5NjYtMi4xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB2aWV3AAAAAAATpP4A
- FF8uABDPFAAD7cwABBMLAANcngAAAAFYWVogAAAAAABMCVYAUAAAAFcf521lYXMAAAAA
- AAAAAQAAAAAAAAAAAAAAAAAAAAAAAAKPAAAAAnNpZyAAAAAAQ1JUIGN1cnYAAAAAAAAE
- AAAAAAUACgAPABQAGQAeACMAKAAtADIANwA7AEAARQBKAE8AVABZAF4AYwBoAG0AcgB3
- AHwAgQCGAIsAkACVAJoAnwCkAKkArgCyALcAvADBAMYAywDQANUA2wDgAOUA6wDwAPYA
- +wEBAQcBDQETARkBHwElASsBMgE4AT4BRQFMAVIBWQFgAWcBbgF1AXwBgwGLAZIBmgGh
- AakBsQG5AcEByQHRAdkB4QHpAfIB+gIDAgwCFAIdAiYCLwI4AkECSwJUAl0CZwJxAnoC
- hAKOApgCogKsArYCwQLLAtUC4ALrAvUDAAMLAxYDIQMtAzgDQwNPA1oDZgNyA34DigOW
- A6IDrgO6A8cD0wPgA+wD+QQGBBMEIAQtBDsESARVBGMEcQR+BIwEmgSoBLYExATTBOEE
- 8AT+BQ0FHAUrBToFSQVYBWcFdwWGBZYFpgW1BcUF1QXlBfYGBgYWBicGNwZIBlkGagZ7
- BowGnQavBsAG0QbjBvUHBwcZBysHPQdPB2EHdAeGB5kHrAe/B9IH5Qf4CAsIHwgyCEYI
- WghuCIIIlgiqCL4I0gjnCPsJEAklCToJTwlkCXkJjwmkCboJzwnlCfsKEQonCj0KVApq
- CoEKmAquCsUK3ArzCwsLIgs5C1ELaQuAC5gLsAvIC+EL+QwSDCoMQwxcDHUMjgynDMAM
- 2QzzDQ0NJg1ADVoNdA2ODakNww3eDfgOEw4uDkkOZA5/DpsOtg7SDu4PCQ8lD0EPXg96
- D5YPsw/PD+wQCRAmEEMQYRB+EJsQuRDXEPURExExEU8RbRGMEaoRyRHoEgcSJhJFEmQS
- hBKjEsMS4xMDEyMTQxNjE4MTpBPFE+UUBhQnFEkUahSLFK0UzhTwFRIVNBVWFXgVmxW9
- FeAWAxYmFkkWbBaPFrIW1hb6Fx0XQRdlF4kXrhfSF/cYGxhAGGUYihivGNUY+hkgGUUZ
- axmRGbcZ3RoEGioaURp3Gp4axRrsGxQbOxtjG4obshvaHAIcKhxSHHscoxzMHPUdHh1H
- HXAdmR3DHeweFh5AHmoelB6+HukfEx8+H2kflB+/H+ogFSBBIGwgmCDEIPAhHCFIIXUh
- oSHOIfsiJyJVIoIiryLdIwojOCNmI5QjwiPwJB8kTSR8JKsk2iUJJTglaCWXJccl9yYn
- Jlcmhya3JugnGCdJJ3onqyfcKA0oPyhxKKIo1CkGKTgpaymdKdAqAio1KmgqmyrPKwIr
- NitpK50r0SwFLDksbiyiLNctDC1BLXYtqy3hLhYuTC6CLrcu7i8kL1ovkS/HL/4wNTBs
- MKQw2zESMUoxgjG6MfIyKjJjMpsy1DMNM0YzfzO4M/E0KzRlNJ402DUTNU01hzXCNf02
- NzZyNq426TckN2A3nDfXOBQ4UDiMOMg5BTlCOX85vDn5OjY6dDqyOu87LTtrO6o76Dwn
- PGU8pDzjPSI9YT2hPeA+ID5gPqA+4D8hP2E/oj/iQCNAZECmQOdBKUFqQaxB7kIwQnJC
- tUL3QzpDfUPARANER0SKRM5FEkVVRZpF3kYiRmdGq0bwRzVHe0fASAVIS0iRSNdJHUlj
- SalJ8Eo3Sn1KxEsMS1NLmkviTCpMcky6TQJNSk2TTdxOJU5uTrdPAE9JT5NP3VAnUHFQ
- u1EGUVBRm1HmUjFSfFLHUxNTX1OqU/ZUQlSPVNtVKFV1VcJWD1ZcVqlW91dEV5JX4Fgv
- WH1Yy1kaWWlZuFoHWlZaplr1W0VblVvlXDVchlzWXSddeF3JXhpebF69Xw9fYV+zYAVg
- V2CqYPxhT2GiYfViSWKcYvBjQ2OXY+tkQGSUZOllPWWSZedmPWaSZuhnPWeTZ+loP2iW
- aOxpQ2maafFqSGqfavdrT2una/9sV2yvbQhtYG25bhJua27Ebx5veG/RcCtwhnDgcTpx
- lXHwcktypnMBc11zuHQUdHB0zHUodYV14XY+dpt2+HdWd7N4EXhueMx5KnmJeed6Rnql
- ewR7Y3vCfCF8gXzhfUF9oX4BfmJ+wn8jf4R/5YBHgKiBCoFrgc2CMIKSgvSDV4O6hB2E
- gITjhUeFq4YOhnKG14c7h5+IBIhpiM6JM4mZif6KZIrKizCLlov8jGOMyo0xjZiN/45m
- js6PNo+ekAaQbpDWkT+RqJIRknqS45NNk7aUIJSKlPSVX5XJljSWn5cKl3WX4JhMmLiZ
- JJmQmfyaaJrVm0Kbr5wcnImc951kndKeQJ6unx2fi5/6oGmg2KFHobaiJqKWowajdqPm
- pFakx6U4pammGqaLpv2nbqfgqFKoxKk3qamqHKqPqwKrdavprFys0K1ErbiuLa6hrxav
- i7AAsHWw6rFgsdayS7LCszizrrQltJy1E7WKtgG2ebbwt2i34LhZuNG5SrnCuju6tbsu
- u6e8IbybvRW9j74KvoS+/796v/XAcMDswWfB48JfwtvDWMPUxFHEzsVLxcjGRsbDx0HH
- v8g9yLzJOsm5yjjKt8s2y7bMNcy1zTXNtc42zrbPN8+40DnQutE80b7SP9LB00TTxtRJ
- 1MvVTtXR1lXW2Ndc1+DYZNjo2WzZ8dp22vvbgNwF3IrdEN2W3hzeot8p36/gNuC94UTh
- zOJT4tvjY+Pr5HPk/OWE5g3mlucf56noMui86Ubp0Opb6uXrcOv77IbtEe2c7ijutO9A
- 78zwWPDl8XLx//KM8xnzp/Q09ML1UPXe9m32+/eK+Bn4qPk4+cf6V/rn+3f8B/yY/Sn9
- uv5L/tz/bf//
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAQAQAAAwAAAAEALAAAAQEAAwAAAAEALAAAAQIAAwAAAAIACAAIAQMAAwAAAAEA
+ AQAAAQYAAwAAAAEAAQAAAQoAAwAAAAEAAQAAAREABAAAAAEAAAAIARIAAwAAAAEAAQAA
+ ARUAAwAAAAEAAgAAARYAAwAAAAEALAAAARcABAAAAAEAAA8gARwAAwAAAAEAAQAAASgA
+ AwAAAAEAAgAAAVIAAwAAAAEAAgAAAVMAAwAAAAIAAQABh3MABwAAEWgAAA/uAAAAAAAA
+ EWhhcHBsAgAAAG1udHJHUkFZWFlaIAfcAAgAFwAPAC4AD2Fjc3BBUFBMAAAAAG5vbmUA
+ AAAAAAAAAAAAAAAAAAAAAAD21gABAAAAANMtYXBwbAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABWRlc2MAAADAAAAAeWRzY20AAAE8AAAH
+ 6GNwcnQAAAkkAAAAI3d0cHQAAAlIAAAAFGtUUkMAAAlcAAAIDGRlc2MAAAAAAAAAH0dl
+ bmVyaWMgR3JheSBHYW1tYSAyLjIgUHJvZmlsZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAABtbHVjAAAAAAAAAB8AAAAMc2tTSwAAAC4AAAGEZGFESwAAADgAAAGyY2FF
+ UwAAADgAAAHqdmlWTgAAAEAAAAIicHRCUgAAAEoAAAJidWtVQQAAACwAAAKsZnJGVQAA
+ AD4AAALYaHVIVQAAADQAAAMWemhUVwAAAB4AAANKbmJOTwAAADoAAANoY3NDWgAAACgA
+ AAOiaGVJTAAAACQAAAPKaXRJVAAAAE4AAAPucm9STwAAACoAAAQ8ZGVERQAAAE4AAARm
+ a29LUgAAACIAAAS0c3ZTRQAAADgAAAGyemhDTgAAAB4AAATWamFKUAAAACYAAAT0ZWxH
+ UgAAACoAAAUacHRQTwAAAFIAAAVEbmxOTAAAAEAAAAWWZXNFUwAAAEwAAAXWdGhUSAAA
+ ADIAAAYidHJUUgAAACQAAAZUZmlGSQAAAEYAAAZ4aHJIUgAAAD4AAAa+cGxQTAAAAEoA
+ AAb8cnVSVQAAADoAAAdGZW5VUwAAADwAAAeAYXJFRwAAACwAAAe8AFYBYQBlAG8AYgBl
+ AGMAbgDhACAAcwBpAHYA4QAgAGcAYQBtAGEAIAAyACwAMgBHAGUAbgBlAHIAaQBzAGsA
+ IABnAHIA5QAgADIALAAyACAAZwBhAG0AbQBhAHAAcgBvAGYAaQBsAEcAYQBtAG0AYQAg
+ AGQAZQAgAGcAcgBpAHMAbwBzACAAZwBlAG4A6AByAGkAYwBhACAAMgAuADIAQx6lAHUA
+ IABoAOwAbgBoACAATQDgAHUAIAB4AOEAbQAgAEMAaAB1AG4AZwAgAEcAYQBtAG0AYQAg
+ ADIALgAyAFAAZQByAGYAaQBsACAARwBlAG4A6QByAGkAYwBvACAAZABhACAARwBhAG0A
+ YQAgAGQAZQAgAEMAaQBuAHoAYQBzACAAMgAsADIEFwQwBDMEMAQ7BEwEPQQwACAARwBy
+ AGEAeQAtBDMEMAQ8BDAAIAAyAC4AMgBQAHIAbwBmAGkAbAAgAGcA6QBuAOkAcgBpAHEA
+ dQBlACAAZwByAGkAcwAgAGcAYQBtAG0AYQAgADIALAAyAMEAbAB0AGEAbADhAG4AbwBz
+ ACAAcwB6APwAcgBrAGUAIABnAGEAbQBtAGEAIAAyAC4AMpAadShwcJaOUUlepgAgADIA
+ LgAyACCCcl9pY8+P8ABHAGUAbgBlAHIAaQBzAGsAIABnAHIA5QAgAGcAYQBtAG0AYQAg
+ ADIALAAyAC0AcAByAG8AZgBpAGwATwBiAGUAYwBuAOEAIAFhAGUAZADhACAAZwBhAG0A
+ YQAgADIALgAyBdIF0AXeBdQAIAXQBeQF1QXoACAF2wXcBdwF2QAgADIALgAyAFAAcgBv
+ AGYAaQBsAG8AIABnAHIAaQBnAGkAbwAgAGcAZQBuAGUAcgBpAGMAbwAgAGQAZQBsAGwA
+ YQAgAGcAYQBtAG0AYQAgADIALAAyAEcAYQBtAGEAIABnAHIAaQAgAGcAZQBuAGUAcgBp
+ AGMBAwAgADIALAAyAEEAbABsAGcAZQBtAGUAaQBuAGUAcwAgAEcAcgBhAHUAcwB0AHUA
+ ZgBlAG4ALQBQAHIAbwBmAGkAbAAgAEcAYQBtAG0AYQAgADIALAAyx3y8GAAg1ozAyQAg
+ rBC5yAAgADIALgAyACDVBLhc0wzHfGZukBpwcF6mfPtlcAAgADIALgAyACBjz4/wZYdO
+ 9k4AgiwwsDDsMKQwrDDzMN4AIAAyAC4AMgAgMNcw7TDVMKEwpDDrA5MDtQO9A7kDugPM
+ ACADkwO6A8EDuQAgA5MDrAO8A7wDsQAgADIALgAyAFAAZQByAGYAaQBsACAAZwBlAG4A
+ 6QByAGkAYwBvACAAZABlACAAYwBpAG4AegBlAG4AdABvAHMAIABkAGEAIABHAGEAbQBt
+ AGEAIAAyACwAMgBBAGwAZwBlAG0AZQBlAG4AIABnAHIAaQBqAHMAIABnAGEAbQBtAGEA
+ IAAyACwAMgAtAHAAcgBvAGYAaQBlAGwAUABlAHIAZgBpAGwAIABnAGUAbgDpAHIAaQBj
+ AG8AIABkAGUAIABnAGEAbQBtAGEAIABkAGUAIABnAHIAaQBzAGUAcwAgADIALAAyDiMO
+ MQ4HDioONQ5BDgEOIQ4hDjIOQA4BDiMOIg5MDhcOMQ5IDicORA4bACAAMgAuADIARwBl
+ AG4AZQBsACAARwByAGkAIABHAGEAbQBhACAAMgAsADIAWQBsAGUAaQBuAGUAbgAgAGgA
+ YQByAG0AYQBhAG4AIABnAGEAbQBtAGEAIAAyACwAMgAgAC0AcAByAG8AZgBpAGkAbABp
+ AEcAZQBuAGUAcgBpAQ0AawBpACAARwByAGEAeQAgAEcAYQBtAG0AYQAgADIALgAyACAA
+ cAByAG8AZgBpAGwAVQBuAGkAdwBlAHIAcwBhAGwAbgB5ACAAcAByAG8AZgBpAGwAIABz
+ AHoAYQByAG8BWwBjAGkAIABnAGEAbQBtAGEAIAAyACwAMgQeBDEESQQwBE8AIARBBDUE
+ QAQwBE8AIAQzBDAEPAQ8BDAAIAAyACwAMgAtBD8EQAQ+BEQEOAQ7BEwARwBlAG4AZQBy
+ AGkAYwAgAEcAcgBhAHkAIABHAGEAbQBtAGEAIAAyAC4AMgAgAFAAcgBvAGYAaQBsAGUG
+ OgYnBkUGJwAgADIALgAyACAGRAZIBkYAIAYxBkUGJwYvBkoAIAY5BicGRXRleHQAAAAA
+ Q29weXJpZ2h0IEFwcGxlIEluYy4sIDIwMTIAAFhZWiAAAAAAAADzUQABAAAAARbMY3Vy
+ dgAAAAAAAAQAAAAABQAKAA8AFAAZAB4AIwAoAC0AMgA3ADsAQABFAEoATwBUAFkAXgBj
+ AGgAbQByAHcAfACBAIYAiwCQAJUAmgCfAKQAqQCuALIAtwC8AMEAxgDLANAA1QDbAOAA
+ 5QDrAPAA9gD7AQEBBwENARMBGQEfASUBKwEyATgBPgFFAUwBUgFZAWABZwFuAXUBfAGD
+ AYsBkgGaAaEBqQGxAbkBwQHJAdEB2QHhAekB8gH6AgMCDAIUAh0CJgIvAjgCQQJLAlQC
+ XQJnAnECegKEAo4CmAKiAqwCtgLBAssC1QLgAusC9QMAAwsDFgMhAy0DOANDA08DWgNm
+ A3IDfgOKA5YDogOuA7oDxwPTA+AD7AP5BAYEEwQgBC0EOwRIBFUEYwRxBH4EjASaBKgE
+ tgTEBNME4QTwBP4FDQUcBSsFOgVJBVgFZwV3BYYFlgWmBbUFxQXVBeUF9gYGBhYGJwY3
+ BkgGWQZqBnsGjAadBq8GwAbRBuMG9QcHBxkHKwc9B08HYQd0B4YHmQesB78H0gflB/gI
+ CwgfCDIIRghaCG4IggiWCKoIvgjSCOcI+wkQCSUJOglPCWQJeQmPCaQJugnPCeUJ+woR
+ CicKPQpUCmoKgQqYCq4KxQrcCvMLCwsiCzkLUQtpC4ALmAuwC8gL4Qv5DBIMKgxDDFwM
+ dQyODKcMwAzZDPMNDQ0mDUANWg10DY4NqQ3DDd4N+A4TDi4OSQ5kDn8Omw62DtIO7g8J
+ DyUPQQ9eD3oPlg+zD88P7BAJECYQQxBhEH4QmxC5ENcQ9RETETERTxFtEYwRqhHJEegS
+ BxImEkUSZBKEEqMSwxLjEwMTIxNDE2MTgxOkE8UT5RQGFCcUSRRqFIsUrRTOFPAVEhU0
+ FVYVeBWbFb0V4BYDFiYWSRZsFo8WshbWFvoXHRdBF2UXiReuF9IX9xgbGEAYZRiKGK8Y
+ 1Rj6GSAZRRlrGZEZtxndGgQaKhpRGncanhrFGuwbFBs7G2MbihuyG9ocAhwqHFIcexyj
+ HMwc9R0eHUcdcB2ZHcMd7B4WHkAeah6UHr4e6R8THz4faR+UH78f6iAVIEEgbCCYIMQg
+ 8CEcIUghdSGhIc4h+yInIlUigiKvIt0jCiM4I2YjlCPCI/AkHyRNJHwkqyTaJQklOCVo
+ JZclxyX3JicmVyaHJrcm6CcYJ0kneierJ9woDSg/KHEooijUKQYpOClrKZ0p0CoCKjUq
+ aCqbKs8rAis2K2krnSvRLAUsOSxuLKIs1y0MLUEtdi2rLeEuFi5MLoIuty7uLyQvWi+R
+ L8cv/jA1MGwwpDDbMRIxSjGCMbox8jIqMmMymzLUMw0zRjN/M7gz8TQrNGU0njTYNRM1
+ TTWHNcI1/TY3NnI2rjbpNyQ3YDecN9c4FDhQOIw4yDkFOUI5fzm8Ofk6Njp0OrI67zst
+ O2s7qjvoPCc8ZTykPOM9Ij1hPaE94D4gPmA+oD7gPyE/YT+iP+JAI0BkQKZA50EpQWpB
+ rEHuQjBCckK1QvdDOkN9Q8BEA0RHRIpEzkUSRVVFmkXeRiJGZ0arRvBHNUd7R8BIBUhL
+ SJFI10kdSWNJqUnwSjdKfUrESwxLU0uaS+JMKkxyTLpNAk1KTZNN3E4lTm5Ot08AT0lP
+ k0/dUCdQcVC7UQZRUFGbUeZSMVJ8UsdTE1NfU6pT9lRCVI9U21UoVXVVwlYPVlxWqVb3
+ V0RXklfgWC9YfVjLWRpZaVm4WgdaVlqmWvVbRVuVW+VcNVyGXNZdJ114XcleGl5sXr1f
+ D19hX7NgBWBXYKpg/GFPYaJh9WJJYpxi8GNDY5dj62RAZJRk6WU9ZZJl52Y9ZpJm6Gc9
+ Z5Nn6Wg/aJZo7GlDaZpp8WpIap9q92tPa6dr/2xXbK9tCG1gbbluEm5rbsRvHm94b9Fw
+ K3CGcOBxOnGVcfByS3KmcwFzXXO4dBR0cHTMdSh1hXXhdj52m3b4d1Z3s3gReG54zHkq
+ eYl553pGeqV7BHtje8J8IXyBfOF9QX2hfgF+Yn7CfyN/hH/lgEeAqIEKgWuBzYIwgpKC
+ 9INXg7qEHYSAhOOFR4Wrhg6GcobXhzuHn4gEiGmIzokziZmJ/opkisqLMIuWi/yMY4zK
+ jTGNmI3/jmaOzo82j56QBpBukNaRP5GokhGSepLjk02TtpQglIqU9JVflcmWNJaflwqX
+ dZfgmEyYuJkkmZCZ/JpomtWbQpuvnByciZz3nWSd0p5Anq6fHZ+Ln/qgaaDYoUehtqIm
+ opajBqN2o+akVqTHpTilqaYapoum/adup+CoUqjEqTepqaocqo+rAqt1q+msXKzQrUSt
+ uK4trqGvFq+LsACwdbDqsWCx1rJLssKzOLOutCW0nLUTtYq2AbZ5tvC3aLfguFm40blK
+ ucK6O7q1uy67p7whvJu9Fb2Pvgq+hL7/v3q/9cBwwOzBZ8Hjwl/C28NYw9TEUcTOxUvF
+ yMZGxsPHQce/yD3IvMk6ybnKOMq3yzbLtsw1zLXNNc21zjbOts83z7jQOdC60TzRvtI/
+ 0sHTRNPG1EnUy9VO1dHWVdbY11zX4Nhk2OjZbNnx2nba+9uA3AXcit0Q3ZbeHN6i3ynf
+ r+A24L3hROHM4lPi2+Nj4+vkc+T85YTmDeaW5x/nqegy6LzpRunQ6lvq5etw6/vshu0R
+ 7ZzuKO6070DvzPBY8OXxcvH/8ozzGfOn9DT0wvVQ9d72bfb794r4Gfio+Tj5x/pX+uf7
+ d/wH/Jj9Kf26/kv+3P9t//8=
StatusItemIconIsTemplate
@@ -267,9 +267,7 @@
public.item
public.folder
- UseXMLPlistFormat
-
Version
- 3.0.1
+ 4.0.0
diff --git a/service/Crunch Image(s).workflow/Contents/QuickLook/Preview.png b/service/Crunch Image(s).workflow/Contents/QuickLook/Preview.png
index ac1f806..1c08a9b 100644
Binary files a/service/Crunch Image(s).workflow/Contents/QuickLook/Preview.png and b/service/Crunch Image(s).workflow/Contents/QuickLook/Preview.png differ
diff --git a/service/Crunch Image(s).workflow/Contents/document.wflow b/service/Crunch Image(s).workflow/Contents/document.wflow
index d210fad..4d334e1 100644
--- a/service/Crunch Image(s).workflow/Contents/document.wflow
+++ b/service/Crunch Image(s).workflow/Contents/document.wflow
@@ -61,7 +61,7 @@
COMMAND_STRING
-/Applications/Crunch.app/Contents/Resources/crunch.py --service $@
+/Applications/Crunch.app/Contents/Resources/crunch.py --service "$@"
CheckedForUserDefaultShell
inputMethod
diff --git a/src/crunch-gui.sh b/src/crunch-gui.sh
index 59687c3..2099805 100755
--- a/src/crunch-gui.sh
+++ b/src/crunch-gui.sh
@@ -15,6 +15,10 @@
#
# ///////////////////////////////////////////////////////////
+# UNCOMMENT FOR TESTING ONLY
+# python crunch.py --gui "$@"
+# exit 0
+
# Message on application open (no arguments passed to script on initial open)
if [ $# -eq 0 ]; then
cat waiting.html
diff --git a/src/crunch.py b/src/crunch.py
index d3d1944..71a4b59 100755
--- a/src/crunch.py
+++ b/src/crunch.py
@@ -5,7 +5,7 @@
# crunch
# A PNG file optimization tool built on pngquant and zopflipng
#
-# Copyright 2018 Christopher Simpkins
+# Copyright 2019 Christopher Simpkins
# MIT License
#
# Source Repository: https://github.com/chrissimpkins/Crunch
@@ -25,6 +25,10 @@
stdstream_lock = Lock()
logging_lock = Lock()
+# Application Constants
+VERSION = "4.0.0"
+VERSION_STRING = "crunch v" + VERSION
+
# Processor Constant
# - Modify this to an integer value if you want to fix the number of
# processes spawned during execution. The process number is
@@ -44,18 +48,14 @@
# Log File Path Constants
LOGFILE_PATH = os.path.join(CRUNCH_DOT_DIRECTORY, "crunch.log")
-# Application Constants
-VERSION = "3.0.1"
-VERSION_STRING = "crunch v" + VERSION
-
HELP_STRING = """
-///////////////////////////////////////////////////
+==================================================
crunch
- Copyright 2018 Christopher Simpkins
+ Copyright 2019 Christopher Simpkins
MIT License
Source: https://github.com/chrissimpkins/Crunch
-///////////////////////////////////////////////////
+==================================================
crunch is a command line executable that performs lossy optimization of one or more png image files with pngquant and zopflipng.
@@ -70,25 +70,33 @@
USAGE = "$ crunch [image path 1]...[image path n]"
-# Create the Crunch dot directory in $HOME if it does not exist
-# Only used for macOS GUI and macOS right-click menu service execution
-if sys.argv[1] in ("--gui", "--service"):
- if not os.path.isdir(CRUNCH_DOT_DIRECTORY):
- os.makedirs(CRUNCH_DOT_DIRECTORY)
- # clear the text in the log file before every script execution
- # logging is only maintained for the last execution of the script
- open(LOGFILE_PATH, "w").close()
-
def main(argv):
+ # Create the Crunch dot directory in $HOME if it does not exist
+ # Only used for macOS GUI and macOS right-click menu service execution
+ if len(argv) > 0 and argv[0] in ("--gui", "--service"):
+ if not os.path.isdir(CRUNCH_DOT_DIRECTORY):
+ os.makedirs(CRUNCH_DOT_DIRECTORY)
+ # clear the text in the log file before every script execution
+ # logging is only maintained for the last execution of the script
+ open(LOGFILE_PATH, "w").close()
+
+ # ////////////////////////
+ # ANSI COLOR DEFINITIONS
+ # ////////////////////////
+ if not is_gui(sys.argv):
+ ERROR_STRING = "[ " + format_ansi_red("!") + " ]"
+ else:
+ ERROR_STRING = "[ ! ]"
+
# //////////////////////////////////
# CONFIRM ARGUMENT PRESENT
# //////////////////////////////////
if len(argv) == 0:
sys.stderr.write(
- "[ERROR] Please include one or more paths to PNG image files as "
+ ERROR_STRING + " Please include one or more paths to PNG image files as "
"arguments to the script." + os.linesep
)
sys.exit(1)
@@ -130,42 +138,65 @@ def main(argv):
for png_path in png_path_list:
# Not a file test
if not os.path.isfile(png_path): # is not an existing file
- sys.stderr.write("[ERROR] '" + png_path + "' does not appear to be a valid path to a PNG file" + os.linesep)
+ sys.stderr.write(
+ ERROR_STRING
+ + " '"
+ + png_path
+ + "' does not appear to be a valid path to a PNG file"
+ + os.linesep
+ )
sys.exit(1) # not a file, abort immediately
# PNG validity test
if not is_valid_png(png_path):
- sys.stderr.write("[ERROR] '" + png_path + "' is not a valid PNG file." + os.linesep)
+ sys.stderr.write(
+ ERROR_STRING
+ + " '"
+ + png_path
+ + "' is not a valid PNG file."
+ + os.linesep
+ )
if is_gui(argv):
log_error(png_path + " is not a valid PNG file.")
NOTPNG_ERROR_FOUND = True
# Exit after checking all file requests and reporting on all invalid file paths (above)
if NOTPNG_ERROR_FOUND is True:
- sys.stderr.write("The request was not executed successfully. Please try again with one or more valid PNG files." + os.linesep)
+ sys.stderr.write(
+ "The request was not executed successfully. Please try again with one or more valid PNG files."
+ + os.linesep
+ )
if is_gui(argv):
- log_error("The request was not executed successfully. Please try again with one or more valid PNG files.")
+ log_error(
+ "The request was not executed successfully. Please try again with one or more valid PNG files."
+ )
sys.exit(1)
# Dependency error handling
if not os.path.exists(PNGQUANT_EXE_PATH):
sys.stderr.write(
- "[ERROR] pngquant executable was not identified on path '"
+ ERROR_STRING
+ + " pngquant executable was not identified on path '"
+ PNGQUANT_EXE_PATH
+ "'"
+ os.linesep
)
if is_gui(argv):
- log_error("pngquant was not found on the expected path " + PNGQUANT_EXE_PATH)
+ log_error(
+ "pngquant was not found on the expected path " + PNGQUANT_EXE_PATH
+ )
sys.exit(1)
elif not os.path.exists(ZOPFLIPNG_EXE_PATH):
sys.stderr.write(
- "[ERROR] zopflipng executable was not identified on path '"
+ ERROR_STRING
+ + " zopflipng executable was not identified on path '"
+ ZOPFLIPNG_EXE_PATH
+ "'"
+ os.linesep
)
if is_gui(argv):
- log_error("zopflipng was not found on the expected path " + ZOPFLIPNG_EXE_PATH)
+ log_error(
+ "zopflipng was not found on the expected path " + ZOPFLIPNG_EXE_PATH
+ )
sys.exit(1)
# ////////////////////////////////////
@@ -186,14 +217,24 @@ def main(argv):
if processes > len(png_path_list):
processes = len(png_path_list)
- print("Spawning " + str(processes) + " processes to optimize " + str(len(png_path_list)) + " image files...")
+ print(
+ "Spawning "
+ + str(processes)
+ + " processes to optimize "
+ + str(len(png_path_list))
+ + " image files..."
+ )
p = Pool(processes)
try:
p.map(optimize_png, png_path_list)
except Exception as e:
stdstream_lock.acquire()
sys.stderr.write("-----" + os.linesep)
- sys.stderr.write("[ERROR] Error detected during execution of the request." + os.linesep)
+ sys.stderr.write(
+ ERROR_STRING
+ + " Error detected during execution of the request."
+ + os.linesep
+ )
sys.stderr.write(str(e) + os.linesep)
stdstream_lock.release()
if is_gui(argv):
@@ -213,15 +254,28 @@ def main(argv):
def optimize_png(png_path):
img = ImageFile(png_path)
+
# define pngquant and zopflipng paths
PNGQUANT_EXE_PATH = get_pngquant_path()
ZOPFLIPNG_EXE_PATH = get_zopflipng_path()
+ # ////////////////////////
+ # ANSI COLOR DEFINITIONS
+ # ////////////////////////
+ if not is_gui(sys.argv):
+ ERROR_STRING = "[ " + format_ansi_red("!") + " ]"
+ else:
+ ERROR_STRING = "[ ! ]"
+
# --------------
# pngquant stage
# --------------
- pngquant_options = " --quality=80-98 --skip-if-larger --force --strip --speed 1 --ext -crunch.png "
- pngquant_command = PNGQUANT_EXE_PATH + pngquant_options + shellquote(img.pre_filepath)
+ pngquant_options = (
+ " --quality=80-98 --skip-if-larger --force --strip --speed 1 --ext -crunch.png "
+ )
+ pngquant_command = (
+ PNGQUANT_EXE_PATH + pngquant_options + shellquote(img.pre_filepath)
+ )
try:
subprocess.check_output(pngquant_command, stderr=subprocess.STDOUT, shell=True)
except CalledProcessError as cpe:
@@ -237,16 +291,32 @@ def optimize_png(png_path):
pass
else:
stdstream_lock.acquire()
- sys.stderr.write("[ERROR] " + img.pre_filepath + " processing failed at the pngquant stage." + os.linesep)
+ sys.stderr.write(
+ ERROR_STRING
+ + " "
+ + img.pre_filepath
+ + " processing failed at the pngquant stage."
+ + os.linesep
+ )
stdstream_lock.release()
if is_gui(sys.argv):
- log_error(img.pre_filepath + " processing failed at the pngquant stage. " + os.linesep + str(cpe))
+ log_error(
+ img.pre_filepath
+ + " processing failed at the pngquant stage. "
+ + os.linesep
+ + str(cpe)
+ )
return None
else:
raise cpe
except Exception as e:
if is_gui(sys.argv):
- log_error(img.pre_filepath + " processing failed at the pngquant stage. " + os.linesep + str(e))
+ log_error(
+ img.pre_filepath
+ + " processing failed at the pngquant stage. "
+ + os.linesep
+ + str(e)
+ )
return None
else:
raise e
@@ -264,21 +334,43 @@ def optimize_png(png_path):
# filters. This achieves better compression than the default approach for non-quantized PNG
# files, but takes significantly longer (based upon testing by CS)
zopflipng_options = " -y --lossy_transparent "
- zopflipng_command = ZOPFLIPNG_EXE_PATH + zopflipng_options + shellquote(img.post_filepath) + " " + shellquote(img.post_filepath)
+ zopflipng_command = (
+ ZOPFLIPNG_EXE_PATH
+ + zopflipng_options
+ + shellquote(img.post_filepath)
+ + " "
+ + shellquote(img.post_filepath)
+ )
try:
subprocess.check_output(zopflipng_command, stderr=subprocess.STDOUT, shell=True)
except CalledProcessError as cpe:
stdstream_lock.acquire()
- sys.stderr.write("[ERROR] " + img.pre_filepath + " processing failed at the zopflipng stage." + os.linesep)
+ sys.stderr.write(
+ ERROR_STRING
+ + " "
+ + img.pre_filepath
+ + " processing failed at the zopflipng stage."
+ + os.linesep
+ )
stdstream_lock.release()
if is_gui(sys.argv):
- log_error(img.pre_filepath + " processing failed at the zopflipng stage. " + os.linesep + str(cpe))
+ log_error(
+ img.pre_filepath
+ + " processing failed at the zopflipng stage. "
+ + os.linesep
+ + str(cpe)
+ )
return None
else:
raise cpe
except Exception as e:
if is_gui(sys.argv):
- log_error(img.pre_filepath + " processing failed at the pngquant stage. " + os.linesep + str(e))
+ log_error(
+ img.pre_filepath
+ + " processing failed at the pngquant stage. "
+ + os.linesep
+ + str(e)
+ )
return None
else:
raise e
@@ -286,17 +378,41 @@ def optimize_png(png_path):
# Check file size post-optimization and report comparison with pre-optimization file
img.get_post_filesize()
percent = img.get_compression_percent()
- percent_string = '{0:.2f}'.format(percent)
+ percent_string = "{0:.2f}%".format(percent)
+ # if compression occurred, color the percent string green
+ # otherwise, leave it default text color
+ if not is_gui(sys.argv) and percent < 100:
+ percent_string = format_ansi_green(percent_string)
# report percent original file size / post file path / size (bytes) to stdout (command line executable)
stdstream_lock.acquire()
- print("[ " + percent_string + "% ] " + img.post_filepath + " (" + str(img.post_size) + " bytes)")
+ print(
+ "[ "
+ + percent_string
+ + " ] "
+ + img.post_filepath
+ + " ("
+ + str(img.post_size)
+ + " bytes)"
+ )
stdstream_lock.release()
- # report percent original file size / post file path / size (bytes) to stdout (macOS GUI + right-click service)
+ # report percent original file size / post file path / size (bytes) to log file (macOS GUI + right-click service)
if is_gui(sys.argv):
- log_info("[ " + percent_string + "% ] " +
- img.post_filepath + " (" + str(img.post_size) + " bytes)")
+ log_info(
+ "[ "
+ + percent_string
+ + " ] "
+ + img.post_filepath
+ + " ("
+ + str(img.post_size)
+ + " bytes)"
+ )
+
+
+# -----------
+# Utilities
+# -----------
def fix_filepath_args(args):
@@ -342,14 +458,14 @@ def get_zopflipng_path():
def is_gui(arglist):
- return ("--gui" in arglist or "--service" in arglist)
+ return "--gui" in arglist or "--service" in arglist
def is_valid_png(filepath):
# The PNG byte signature (https://www.w3.org/TR/PNG/#5PNG-file-signature)
- expected_signature = struct.pack('8B', 137, 80, 78, 71, 13, 10, 26, 10)
+ expected_signature = struct.pack("8B", 137, 80, 78, 71, 13, 10, 26, 10)
# open the file and read first 8 bytes
- with open(filepath, 'rb') as filer:
+ with open(filepath, "rb") as filer:
signature = filer.read(8)
# return boolean test result for first eight bytes == expected PNG byte signature
return signature == expected_signature
@@ -358,7 +474,7 @@ def is_valid_png(filepath):
def log_error(errmsg):
current_time = time.strftime("%m-%d-%y %H:%M:%S")
logging_lock.acquire()
- with open(LOGFILE_PATH, 'a') as filewriter:
+ with open(LOGFILE_PATH, "a") as filewriter:
filewriter.write(current_time + "\tERROR\t" + errmsg + os.linesep)
filewriter.flush()
os.fsync(filewriter.fileno())
@@ -368,7 +484,7 @@ def log_error(errmsg):
def log_info(infomsg):
current_time = time.strftime("%m-%d-%y %H:%M:%S")
logging_lock.acquire()
- with open(LOGFILE_PATH, 'a') as filewriter:
+ with open(LOGFILE_PATH, "a") as filewriter:
filewriter.write(current_time + "\tINFO\t" + infomsg + os.linesep)
filewriter.flush()
os.fsync(filewriter.fileno())
@@ -380,6 +496,20 @@ def shellquote(filepath):
return "'" + filepath.replace("'", "'\\''") + "'"
+def format_ansi_red(text):
+ if sys.stdout.isatty():
+ return "\033[0;31m" + text + "\033[0m"
+ else:
+ return text
+
+
+def format_ansi_green(text):
+ if sys.stdout.isatty():
+ return "\033[0;32m" + text + "\033[0m"
+ else:
+ return text
+
+
# ///////////////////////
# OBJECT DEFINITIONS
# ///////////////////////
@@ -415,7 +545,7 @@ def get_compression_percent(self):
# This workaround reconstructs the original filepaths
# that are split by the shell script into separate arguments
# when there are spaces in the macOS file path
- if sys.argv[1] in ("--gui", "--service"):
+ if len(sys.argv) > 1 and sys.argv[1] in ("--gui", "--service"):
arg_list = fix_filepath_args(sys.argv[1:])
main(arg_list)
else:
diff --git a/src/include/pngquant b/src/include/pngquant
index d8db8d2..5f68d8d 100755
Binary files a/src/include/pngquant and b/src/include/pngquant differ
diff --git a/src/include/zopflipng b/src/include/zopflipng
index f1caf2e..ac750b9 100755
Binary files a/src/include/zopflipng and b/src/include/zopflipng differ
diff --git a/src/install-dependencies.sh b/src/install-dependencies.sh
index 001126f..7d40940 100755
--- a/src/install-dependencies.sh
+++ b/src/install-dependencies.sh
@@ -15,9 +15,9 @@ PNGQUANT_EXE="$PNGQUANT_BUILD_DIR/pngquant"
ZOPFLIPNG_BUILD_DIR="$HOME/zopfli"
ZOPFLIPNG_EXE="$ZOPFLIPNG_BUILD_DIR/zopflipng"
-PNGQUANT_VERSION_TAG="2.12.0"
-ZOPFLIPNG_VERSION_TAG="v2.1.0"
-LIBPNG_VERSION="1.6.34"
+PNGQUANT_VERSION_TAG="2.12.5"
+ZOPFLIPNG_VERSION_TAG="v2.2.0"
+LIBPNG_VERSION="1.6.37"
LIBPNG_VERSION_DOWNLOAD="libpng16/$LIBPNG_VERSION/libpng-$LIBPNG_VERSION.tar.xz"
LITTLECMS_VERSION="2.9"
diff --git a/src/test_crunch_errors.py b/src/test_crunch_errors.py
index 86bf227..873a77f 100644
--- a/src/test_crunch_errors.py
+++ b/src/test_crunch_errors.py
@@ -33,30 +33,33 @@ def test_pytest_capsys(capsys):
def test_crunch_missing_argument_error(capsys):
- with pytest.raises(SystemExit):
+ with pytest.raises(SystemExit) as exit_info:
src.crunch.main([])
out, err = capsys.readouterr()
assert len(err) > 0
- assert err.startswith("[ERROR]") is True
+ assert err.startswith("[ ! ]") is True
+ assert exit_info.value.code == 1
def test_crunch_missing_file_error(capsys):
- with pytest.raises(SystemExit):
+ with pytest.raises(SystemExit) as exit_info:
src.crunch.main(["bogusfile.png"])
out, err = capsys.readouterr()
assert len(err) > 0
- assert err.startswith("[ERROR]") is True
+ assert err.startswith("[ ! ]") is True
+ assert exit_info.value.code == 1
def test_crunch_bad_filepath_error(capsys):
- with pytest.raises(SystemExit):
+ with pytest.raises(SystemExit) as exit_info:
src.crunch.main(["src/test_crunch_errors.py"])
out, err = capsys.readouterr()
assert len(err) > 0
- assert err.startswith("[ERROR]") is True
+ assert err.startswith("[ ! ]") is True
+ assert exit_info.value.code == 1
# ///////////////////////////////////////////////////////
@@ -70,11 +73,12 @@ def return_bogus_path():
return os.path.join("bogus", "pngquant")
monkeypatch.setattr(src.crunch, 'get_pngquant_path', return_bogus_path)
testpath = os.path.join("testfiles", "robot.png")
- with pytest.raises(SystemExit):
+ with pytest.raises(SystemExit) as exit_info:
src.crunch.main([testpath])
out, err = capsys.readouterr()
- assert err.startswith("[ERROR]") is True
+ assert err.startswith("[ ! ]") is True
+ assert exit_info.value.code == 1
def test_crunch_missing_zopflipng_error(capsys, monkeypatch):
@@ -82,11 +86,12 @@ def return_bogus_path():
return os.path.join("bogus", "zopflipng")
monkeypatch.setattr(src.crunch, 'get_zopflipng_path', return_bogus_path)
testpath = os.path.join("testfiles", "robot.png")
- with pytest.raises(SystemExit):
+ with pytest.raises(SystemExit) as exit_info:
src.crunch.main([testpath])
out, err = capsys.readouterr()
- assert err.startswith("[ERROR]") is True
+ assert err.startswith("[ ! ]") is True
+ assert exit_info.value.code == 1
# ///////////////////////////////////////////////////////
@@ -101,8 +106,9 @@ def raise_ioerror():
monkeypatch.setattr(src.crunch, 'optimize_png', raise_ioerror)
testpath1 = os.path.join("testfiles", "robot.png")
testpath2 = os.path.join("testfiles", "robot.png")
- with pytest.raises(SystemExit):
+ with pytest.raises(SystemExit) as exit_info:
src.crunch.main([testpath1, testpath2])
out, err = capsys.readouterr()
- assert "[ERROR]" in err
+ assert "[ ! ]" in err
+ assert exit_info.value.code == 1
diff --git a/src/test_crunch_execution.py b/src/test_crunch_execution.py
index 2660d4b..2a38b4e 100644
--- a/src/test_crunch_execution.py
+++ b/src/test_crunch_execution.py
@@ -3,7 +3,6 @@
import os
import sys
-import platform
import pytest
import shutil
from subprocess import CalledProcessError
@@ -39,7 +38,7 @@ def test_crunch_help_shortoption(capsys):
src.crunch.main(["-h"])
out, err = capsys.readouterr()
- assert out[0:5] == "\n////"
+ assert out[0:5] == "\n===="
def test_crunch_help_longoption(capsys):
@@ -47,7 +46,7 @@ def test_crunch_help_longoption(capsys):
src.crunch.main(["--help"])
out, err = capsys.readouterr()
- assert out[0:5] == "\n////"
+ assert out[0:5] == "\n===="
def test_crunch_usage(capsys):
@@ -242,13 +241,14 @@ def test_crunch_function_optimize_png_unoptimized_file():
os.remove(testpath)
src.crunch.optimize_png(startpath)
- # check for optimized file following execution
+ # check for optimized file following execution
assert os.path.exists(testpath) is True
# cleanup optimized file produced by this test
if os.path.exists(testpath):
os.remove(testpath)
+
def test_crunch_function_optimize_png_preoptimized_file():
startpath = os.path.join("testfiles", "cat-cr.png") # test a file that has previously been optimized
testpath = os.path.join("testfiles", "cat-cr-crunch.png")
@@ -271,13 +271,13 @@ def test_crunch_function_optimize_png_bad_filetype(capsys):
src.crunch.optimize_png(startpath)
out, err = capsys.readouterr()
- assert err[0:7] == "[ERROR]"
+ assert "[ ! ]" in err
# main function
def test_crunch_function_main_single_file():
- with pytest.raises(SystemExit):
+ with pytest.raises(SystemExit) as exit_info:
startpath = os.path.join("testfiles", "robot.png")
testpath = os.path.join("testfiles", "robot-crunch.png")
# cleanup any existing files from previous tests
@@ -287,6 +287,25 @@ def test_crunch_function_main_single_file():
# check for optimized file following execution
assert os.path.exists(testpath) is True
+ assert exit_info.value.code == 0
+
+ # cleanup optimized file produced by this test
+ if os.path.exists(testpath):
+ os.remove(testpath)
+
+
+def test_crunch_function_main_single_file_with_spaces_in_path():
+ with pytest.raises(SystemExit) as exit_info:
+ startpath = os.path.join("testfiles", "img with spaces.png")
+ testpath = os.path.join("testfiles", "img with spaces-crunch.png")
+ # cleanup any existing files from previous tests
+ if os.path.exists(testpath):
+ os.remove(testpath)
+ src.crunch.main([startpath])
+
+ # check for optimized file following execution
+ assert os.path.exists(testpath) is True
+ assert exit_info.value.code == 0
# cleanup optimized file produced by this test
if os.path.exists(testpath):
@@ -294,7 +313,7 @@ def test_crunch_function_main_single_file():
def test_crunch_function_main_multi_file():
- with pytest.raises(SystemExit):
+ with pytest.raises(SystemExit) as exit_info:
startpath1 = os.path.join("testfiles", "robot.png")
startpath2 = os.path.join("testfiles", "cat.png")
testpath1 = os.path.join("testfiles", "robot-crunch.png")
@@ -311,6 +330,7 @@ def test_crunch_function_main_multi_file():
# check for optimized file following execution
assert os.path.exists(testpath1) is True
assert os.path.exists(testpath2) is True
+ assert exit_info.value.code == 0
# cleanup optimized file produced by this test
if os.path.exists(testpath1):
@@ -318,105 +338,158 @@ def test_crunch_function_main_multi_file():
if os.path.exists(testpath2):
os.remove(testpath2)
-
+@pytest.mark.skipif(sys.platform != "darwin", reason="requires macOS platform")
def test_crunch_function_main_single_file_with_gui_flag():
setup_logging_path()
- if platform.system() == "Darwin":
- with pytest.raises(SystemExit):
- startpath = os.path.join("testfiles", "robot.png")
- testpath = os.path.join("testfiles", "robot-crunch.png")
- # cleanup any existing files from previous tests
- if os.path.exists(testpath):
- os.remove(testpath)
- src.crunch.main(["--gui", startpath])
-
- # check for optimized file following execution
- assert os.path.exists(testpath) is True
-
- # cleanup optimized file produced by this test
+
+ with pytest.raises(SystemExit) as exit_info:
+ startpath = os.path.join("testfiles", "robot.png")
+ testpath = os.path.join("testfiles", "robot-crunch.png")
+ # cleanup any existing files from previous tests
if os.path.exists(testpath):
os.remove(testpath)
+ src.crunch.main(["--gui", startpath])
+
+ # check for optimized file following execution
+ assert os.path.exists(testpath) is True
+ assert exit_info.value.code == 0
+
+ # cleanup optimized file produced by this test
+ if os.path.exists(testpath):
+ os.remove(testpath)
teardown_logging_path()
+@pytest.mark.skipif(sys.platform != "darwin", reason="requires macOS platform")
+def test_crunch_function_main_single_file_with_spaces_with_gui_flag():
+ setup_logging_path()
+
+ with pytest.raises(SystemExit) as exit_info:
+ startpath = os.path.join("testfiles", "img with spaces.png")
+ testpath = os.path.join("testfiles", "img with spaces-crunch.png")
+ # cleanup any existing files from previous tests
+ if os.path.exists(testpath):
+ os.remove(testpath)
+ src.crunch.main(["--gui", startpath])
+
+ # check for optimized file following execution
+ assert os.path.exists(testpath) is True
+ assert exit_info.value.code == 0
+
+ # cleanup optimized file produced by this test
+ if os.path.exists(testpath):
+ os.remove(testpath)
+
+ teardown_logging_path()
+
+
+@pytest.mark.skipif(sys.platform != "darwin", reason="requires macOS platform")
def test_crunch_function_main_single_file_with_service_flag():
setup_logging_path()
- if platform.system() == "Darwin":
- with pytest.raises(SystemExit):
- startpath = os.path.join("testfiles", "robot.png")
- testpath = os.path.join("testfiles", "robot-crunch.png")
- # cleanup any existing files from previous tests
- if os.path.exists(testpath):
- os.remove(testpath)
- src.crunch.main(["--service", startpath])
-
- # check for optimized file following execution
- assert os.path.exists(testpath) is True
-
- # cleanup optimized file produced by this test
+
+ with pytest.raises(SystemExit) as exit_info:
+ startpath = os.path.join("testfiles", "robot.png")
+ testpath = os.path.join("testfiles", "robot-crunch.png")
+ # cleanup any existing files from previous tests
if os.path.exists(testpath):
os.remove(testpath)
+ src.crunch.main(["--service", startpath])
+
+ # check for optimized file following execution
+ assert os.path.exists(testpath) is True
+ assert exit_info.value.code == 0
+
+ # cleanup optimized file produced by this test
+ if os.path.exists(testpath):
+ os.remove(testpath)
teardown_logging_path()
+@pytest.mark.skipif(sys.platform != "darwin", reason="requires macOS platform")
+def test_crunch_function_main_single_file_with_spaces_with_service_flag():
+ setup_logging_path()
+
+ with pytest.raises(SystemExit) as exit_info:
+ startpath = os.path.join("testfiles", "img with spaces.png")
+ testpath = os.path.join("testfiles", "img with spaces-crunch.png")
+ # cleanup any existing files from previous tests
+ if os.path.exists(testpath):
+ os.remove(testpath)
+ src.crunch.main(["--service", startpath])
+
+ # check for optimized file following execution
+ assert os.path.exists(testpath) is True
+ assert exit_info.value.code == 0
+
+ # cleanup optimized file produced by this test
+ if os.path.exists(testpath):
+ os.remove(testpath)
+
+ teardown_logging_path()
+
+
+@pytest.mark.skipif(sys.platform != "darwin", reason="requires macOS platform")
def test_crunch_function_main_multi_file_with_gui_flag():
setup_logging_path()
- if platform.system() == "Darwin":
- with pytest.raises(SystemExit):
- startpath1 = os.path.join("testfiles", "robot.png")
- startpath2 = os.path.join("testfiles", "cat.png")
- testpath1 = os.path.join("testfiles", "robot-crunch.png")
- testpath2 = os.path.join("testfiles", "cat-crunch.png")
-
- # cleanup any existing files from previous tests
- if os.path.exists(testpath1):
- os.remove(testpath1)
- if os.path.exists(testpath2):
- os.remove(testpath2)
-
- src.crunch.main(["--gui", startpath1, startpath2])
-
- # check for optimized file following execution
- assert os.path.exists(testpath1) is True
- assert os.path.exists(testpath2) is True
-
- # cleanup optimized file produced by this test
+
+ with pytest.raises(SystemExit) as exit_info:
+ startpath1 = os.path.join("testfiles", "robot.png")
+ startpath2 = os.path.join("testfiles", "cat.png")
+ testpath1 = os.path.join("testfiles", "robot-crunch.png")
+ testpath2 = os.path.join("testfiles", "cat-crunch.png")
+
+ # cleanup any existing files from previous tests
if os.path.exists(testpath1):
os.remove(testpath1)
if os.path.exists(testpath2):
os.remove(testpath2)
+ src.crunch.main(["--gui", startpath1, startpath2])
+
+ # check for optimized file following execution
+ assert os.path.exists(testpath1) is True
+ assert os.path.exists(testpath2) is True
+ assert exit_info.value.code == 0
+
+ # cleanup optimized file produced by this test
+ if os.path.exists(testpath1):
+ os.remove(testpath1)
+ if os.path.exists(testpath2):
+ os.remove(testpath2)
+
teardown_logging_path()
+@pytest.mark.skipif(sys.platform != "darwin", reason="requires macOS platform")
def test_crunch_function_main_multi_file_with_service_flag():
setup_logging_path()
- if platform.system() == "Darwin":
- with pytest.raises(SystemExit):
- startpath1 = os.path.join("testfiles", "robot.png")
- startpath2 = os.path.join("testfiles", "cat.png")
- testpath1 = os.path.join("testfiles", "robot-crunch.png")
- testpath2 = os.path.join("testfiles", "cat-crunch.png")
-
- # cleanup any existing files from previous tests
- if os.path.exists(testpath1):
- os.remove(testpath1)
- if os.path.exists(testpath2):
- os.remove(testpath2)
-
- src.crunch.main(["--service", startpath1, startpath2])
-
- # check for optimized file following execution
- assert os.path.exists(testpath1) is True
- assert os.path.exists(testpath2) is True
-
- # cleanup optimized file produced by this test
+
+ with pytest.raises(SystemExit) as exit_info:
+ startpath1 = os.path.join("testfiles", "robot.png")
+ startpath2 = os.path.join("testfiles", "cat.png")
+ testpath1 = os.path.join("testfiles", "robot-crunch.png")
+ testpath2 = os.path.join("testfiles", "cat-crunch.png")
+
+ # cleanup any existing files from previous tests
if os.path.exists(testpath1):
os.remove(testpath1)
if os.path.exists(testpath2):
os.remove(testpath2)
+
+ src.crunch.main(["--service", startpath1, startpath2])
+
+ # check for optimized file following execution
+ assert os.path.exists(testpath1) is True
+ assert os.path.exists(testpath2) is True
+ assert exit_info.value.code == 0
+
+ # cleanup optimized file produced by this test
+ if os.path.exists(testpath1):
+ os.remove(testpath1)
+ if os.path.exists(testpath2):
+ os.remove(testpath2)
teardown_logging_path()
@@ -454,8 +527,44 @@ def test_crunch_log_info():
teardown_logging_path()
+@pytest.mark.skipif(sys.platform != "darwin", reason="requires macOS platform")
+def test_crunch_log_from_main_with_service():
+ teardown_logging_path()
+ with pytest.raises(SystemExit) as exit_info:
+ startpath1 = os.path.join("testfiles", "robot.png")
+ startpath2 = os.path.join("testfiles", "cat.png")
+ testpath1 = os.path.join("testfiles", "robot-crunch.png")
+ testpath2 = os.path.join("testfiles", "cat-crunch.png")
+ logpath = src.crunch.LOGFILE_PATH
+
+ # cleanup any existing files from previous tests
+ if os.path.exists(testpath1):
+ os.remove(testpath1)
+ if os.path.exists(testpath2):
+ os.remove(testpath2)
+
+ src.crunch.main(["--service", startpath1, startpath2])
+
+ # check for presence of log file
+ assert os.path.exists(logpath)
+ with open(logpath, "r") as f:
+ text = f.read()
+ assert "Crunch execution ended." in text
+ assert exit_info.value.code == 0
+
+ # cleanup optimized file produced by this test
+ if os.path.exists(testpath1):
+ os.remove(testpath1)
+ if os.path.exists(testpath2):
+ os.remove(testpath2)
+
+ teardown_logging_path()
+
+#
# Utility functions
+#
+
def setup_logging_path():
# setup the logging directory
diff --git a/src/utils/dssim-comparisons.sh b/src/utils/dssim-comparisons.sh
new file mode 100755
index 0000000..9a994b2
--- /dev/null
+++ b/src/utils/dssim-comparisons.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+
+echo "Cat image"
+dssim "../../img/cat-1285634_640.png" "../../img/cat-1285634_640-crunch.png"
+
+# sun's rays image
+echo "\n\nSun image"
+dssim "../../img/suns-rays-478249_640.png" "../../img/suns-rays-478249_640-crunch.png"
+
+# prairie image
+echo "\n\nPrairie image"
+dssim "../../img/prairie-679014_640.png" "../../img/prairie-679014_640-crunch.png"
+
+# Robot image
+echo "\n\nRobot image"
+dssim "../../img/robot-1214536_640.png" "../../img/robot-1214536_640-crunch.png"
+
+# Color circule image
+echo "\n\nColor circle image"
+dssim "../../img/colors-157474_640.png" "../../img/colors-157474_640-crunch.png"
+
+# Flowers image
+echo "\n\nFlowers image"
+dssim "../../img/flowers-67839_640.png" "../../img/flowers-67839_640-crunch.png"
+
diff --git a/src/utils/image-compare.py b/src/utils/image-compare.py
new file mode 100644
index 0000000..ab22530
--- /dev/null
+++ b/src/utils/image-compare.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python3
+
+import os
+from pathlib import Path
+
+
+def print_results(size_pre, size_post):
+ print(f"Pre: {size_pre}")
+ print(f"Post: {size_post}")
+ percent = (size_post / size_pre) * 100
+ print(f"{percent}%")
+
+
+p = Path(".")
+
+# cat image
+print("Cat image")
+pre = os.path.getsize(list(p.glob("../../img/cat-1285634_640.png"))[0])
+post = os.path.getsize(list(p.glob("../../img/cat-1285634_640-crunch.png"))[0])
+print_results(pre, post)
+
+# sun's rays image
+print("\n\nSun image")
+pre = os.path.getsize(list(p.glob("../../img/suns-rays-478249_640.png"))[0])
+post = os.path.getsize(list(p.glob("../../img/suns-rays-478249_640-crunch.png"))[0])
+print_results(pre, post)
+
+# prairie image
+print("\n\nPrairie image")
+pre = os.path.getsize(list(p.glob("../../img/prairie-679014_640.png"))[0])
+post = os.path.getsize(list(p.glob("../../img/prairie-679014_640-crunch.png"))[0])
+print_results(pre, post)
+
+# Robot image
+print("\n\nRobot image")
+pre = os.path.getsize(list(p.glob("../../img/robot-1214536_640.png"))[0])
+post = os.path.getsize(list(p.glob("../../img/robot-1214536_640-crunch.png"))[0])
+print_results(pre, post)
+
+# Color circule image
+print("\n\nColor circle image")
+pre = os.path.getsize(list(p.glob("../../img/colors-157474_640.png"))[0])
+post = os.path.getsize(list(p.glob("../../img/colors-157474_640-crunch.png"))[0])
+print_results(pre, post)
+
+# Flowers image
+print("\n\nFlowers image")
+pre = os.path.getsize(list(p.glob("../../img/flowers-67839_640.png"))[0])
+post = os.path.getsize(list(p.glob("../../img/flowers-67839_640-crunch.png"))[0])
+print_results(pre, post)
+
diff --git a/testfiles/cat-cr.png b/testfiles/cat-cr.png
index 25b49aa..7698642 100644
Binary files a/testfiles/cat-cr.png and b/testfiles/cat-cr.png differ
diff --git a/testfiles/img with spaces.png b/testfiles/img with spaces.png
new file mode 100644
index 0000000..963a299
Binary files /dev/null and b/testfiles/img with spaces.png differ
diff --git a/tox.ini b/tox.ini
index 70eddba..2425b15 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
[tox]
skipsdist = True
-envlist = py27,py36
+envlist = py27,py37
[testenv]
deps = pytest