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 @@ Crunch PNG file optimization [![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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 + 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