diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 546b793..3b255d6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -16,8 +16,8 @@ Steps to reproduce the behavior: **Expected behavior** A clear and concise description of what you expected to happen. -**Screenshots** -If applicable, add screenshots to help explain your problem. +**Screenshots and/or Log Text** +If applicable, add screenshots to help explain your problem. For macOS GUI and macOS right-click menu service problems, please copy and paste the text from the log file `~/.crunch/crunch.log` into your bug report (requires Crunch v3.0.0 or higher). **Desktop (please complete the following information):** - OS: [e.g. macOS] diff --git a/CHANGELOG.md b/CHANGELOG.md index c0804f6..2460d14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ ## Changelog +### v3.0.0 + +- improved quality of pngquant quantization of PNG images across all file sizes +- upgraded embedded pngquant to v2.12.0 (includes reduced pngquant optimization times) +- converted to custom build of zopflipng that is modified for use in the Crunch applications (forked from google/zopfli at git tag zopfli-1.0.2) at git version tag v2.1.0 (source repository is chrissimpkins/zopfli) +- improved zopfli compression ratios for post-quantized and non-quantized in-file sizes under 350kB. Many files are ~33% original file size after they are quantized with pngquant so this affects pre-optimization files up to just over ~1MB in size when the pngquant step is completed (the quantize step yields a modified image binary when it does not lead to larger file size or image quality below Crunch project thresholds, when this does not occur the original file at the original file size is used as the in-file to zopflipng) +- improved zopfli compression speed for post-quantized and non-quantized in-file sizes over 750kB +- eliminate optional PNG chunks by default in all files (reduces file size) +- converted to use of PNG filter = 0 for zopflipng compression of all quantized files (increases compression speed) +- use automated detection of best PNG filter for zopfli compression in all non-quantized files (improves compression) +- remove hidden colors behind alpha channel 0 in files that are not quantized due to low quality or increased file size following pngquant runs +- added new macOS GUI animations with success and fail indicators (thanks Gary Jacobs!) +- added logging of compression data and errors in macOS GUI and macOS right-click menu service tools in a new log file that is generated on the path `~/.crunch/crunch.log` +- updated redirect to /dev/null in install-dependencies.sh compile script for POSIX compliance +- refactored command line option parsing code (thanks Chris Clauss!) +- added new bug reporting template + ### v2.1.0 - added automated detection of png image types through read of PNG file signatures diff --git a/README.md b/README.md index 35423a3..57efc01 100644 --- a/README.md +++ b/README.md @@ -50,16 +50,16 @@ 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. For example, view the horizon line + clouds in the prairie photo below for a demonstration of the introduction of undesirable image artifacts in the image. 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. 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. ## Photography Examples ### Cat Image - Original Size: 583,398 bytes -- Optimized Size: 195,430 bytes -- DSSIM similarity score: 0.001504 -- Percent original size: 33.50% +- Optimized Size: 196,085 bytes +- DSSIM similarity score: 0.001471 +- Percent original size: 33.61% ##### Original @@ -72,9 +72,9 @@ The following examples demonstrate the benefits and disadvantages of the current ### Sun's Rays - Original Size: 138,272 -- Optimized Size: 64,982 -- DSSIM similarity score: 0.000913 -- Percent original size: 47.00% +- Optimized Size: 66,593 +- DSSIM similarity score: 0.000948 +- Percent original size: 48.16% ##### Original @@ -88,7 +88,7 @@ The following examples demonstrate the benefits and disadvantages of the current ### Prairie Image - Original Size: 196,794 bytes -- Optimized Size: 77,968 bytes +- Optimized Size: 77,965 bytes - DSSIM similarity score: 0.002988 - Percent original size: 39.62% @@ -107,9 +107,9 @@ The following examples demonstrate the benefits and disadvantages of the current ### Robot Image - Original Size: 197,193 bytes -- Optimized Size: 67,773 bytes -- DSSIM similarity score: 0.000163 -- Percent original size: 34.37% +- Optimized Size: 67,596 bytes +- DSSIM similarity score: 0.000162 +- Percent original size: 34.28% ##### Original @@ -122,9 +122,9 @@ The following examples demonstrate the benefits and disadvantages of the current ### Color Circle Image - Original Size: 249,251 bytes -- Optimized Size: 67,326 bytes -- DSSIM similarity score: 0.002503 -- Percent original size: 27.01% +- Optimized Size: 67,135 bytes +- DSSIM similarity score: 0.002491 +- Percent original size: 26.93% ##### Original @@ -138,9 +138,9 @@ The following examples demonstrate the benefits and disadvantages of the current ### Flowers Image - Original Size: 440,126 bytes -- Optimized Size: 196,979 bytes -- DSSIM similarity score: 0.000481 -- Percent original size: 44.76% +- Optimized Size: 196,962 bytes +- DSSIM similarity score: 0.000480 +- Percent original size: 44.75% ##### Original @@ -164,7 +164,7 @@ Crunch is licensed under the [MIT license](https://github.com/chrissimpkins/Crun pngquant is licensed under the [Gnu General Public License, version 3](https://github.com/pornel/pngquant/blob/master/COPYRIGHT). The pngquant source code is available [here](https://github.com/pornel/pngquant). -zopflipng is licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0). The zopflipng source code is available [here](https://github.com/google/zopfli). +zopflipng is licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0). The upstream zopflipng source code is available [here](https://github.com/google/zopfli). The source for the modified zopflipng fork that is used in this project is available [here](https://github.com/chrissimpkins/zopfli). See the [LICENSE.md](LICENSE.md) document for details and additional licensing information for this project. @@ -178,3 +178,5 @@ Crunch is a simple tool that makes excellent, free, open source software built b - Lode Vandevenne, Jyrki Alakuijala, and the [zopfli project contributors](https://github.com/google/zopfli/graphs/contributors) - Kornel LesiƄski and the [pngquant project contributors](https://github.com/kornelski/pngquant/graphs/contributors) + +The fantastic macOS GUI animations were designed by [Gary Jacobs](https://github.com/garyjacobs). diff --git a/bin/Crunch.app/Contents/Info.plist b/bin/Crunch.app/Contents/Info.plist index 939b424..8777f0e 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/Resources/01-Load-Up@2x.gif b/bin/Crunch.app/Contents/Resources/01-Load-Up@2x.gif new file mode 100644 index 0000000..85ef2e2 Binary files /dev/null and b/bin/Crunch.app/Contents/Resources/01-Load-Up@2x.gif differ diff --git a/bin/Crunch.app/Contents/Resources/02-Waiting@2x.gif b/bin/Crunch.app/Contents/Resources/02-Waiting@2x.gif new file mode 100644 index 0000000..2b8da05 Binary files /dev/null and b/bin/Crunch.app/Contents/Resources/02-Waiting@2x.gif differ diff --git a/bin/Crunch.app/Contents/Resources/03-Crunching@2x.gif b/bin/Crunch.app/Contents/Resources/03-Crunching@2x.gif new file mode 100644 index 0000000..0f73546 Binary files /dev/null and b/bin/Crunch.app/Contents/Resources/03-Crunching@2x.gif differ diff --git a/bin/Crunch.app/Contents/Resources/04-Error@2x.gif b/bin/Crunch.app/Contents/Resources/04-Error@2x.gif new file mode 100644 index 0000000..78a9816 Binary files /dev/null and b/bin/Crunch.app/Contents/Resources/04-Error@2x.gif differ diff --git a/bin/Crunch.app/Contents/Resources/04-Success@2x.gif b/bin/Crunch.app/Contents/Resources/04-Success@2x.gif new file mode 100644 index 0000000..cc7d0cb Binary files /dev/null and b/bin/Crunch.app/Contents/Resources/04-Success@2x.gif differ diff --git a/bin/Crunch.app/Contents/Resources/Credits.html b/bin/Crunch.app/Contents/Resources/Credits.html index dbf82b2..9d89ddf 100644 --- a/bin/Crunch.app/Contents/Resources/Credits.html +++ b/bin/Crunch.app/Contents/Resources/Credits.html @@ -26,7 +26,7 @@

Upgrade

To upgrade with Homebrew, enter the following command in your terminal:

- $ brew cask uninstall crunch && brew cask install crunch + $ brew cask upgrade

Other Crunch Tools

@@ -57,4 +57,8 @@

Embedded Software Licenses

+

+

Animations

+

The Crunch GUI animations were designed by + Gary Jacobs and are licensed under the MIT License.

\ No newline at end of file diff --git a/bin/Crunch.app/Contents/Resources/clear.html b/bin/Crunch.app/Contents/Resources/clear.html index 3e0651f..fb80407 100644 --- a/bin/Crunch.app/Contents/Resources/clear.html +++ b/bin/Crunch.app/Contents/Resources/clear.html @@ -9,6 +9,7 @@ body, html { overflow: hidden; + background-color: #424242; } .clear { diff --git a/bin/Crunch.app/Contents/Resources/complete.html b/bin/Crunch.app/Contents/Resources/complete-error.html similarity index 74% rename from bin/Crunch.app/Contents/Resources/complete.html rename to bin/Crunch.app/Contents/Resources/complete-error.html index 906d4fd..a6a7c1c 100644 --- a/bin/Crunch.app/Contents/Resources/complete.html +++ b/bin/Crunch.app/Contents/Resources/complete-error.html @@ -9,18 +9,13 @@ body, html { overflow: hidden; - background-color: #FFF; + background-color: #424242; } .frame { - height: 200px; white-space: nowrap; text-align: center; } - - img { - margin-top: 50px; - } @@ -28,7 +23,7 @@
- +
diff --git a/html/complete.html b/bin/Crunch.app/Contents/Resources/complete-success.html similarity index 74% rename from html/complete.html rename to bin/Crunch.app/Contents/Resources/complete-success.html index 906d4fd..6f2643d 100644 --- a/html/complete.html +++ b/bin/Crunch.app/Contents/Resources/complete-success.html @@ -9,18 +9,13 @@ body, html { overflow: hidden; - background-color: #FFF; + background-color: #424242; } .frame { - height: 200px; white-space: nowrap; text-align: center; } - - img { - margin-top: 50px; - } @@ -28,7 +23,7 @@
- +
diff --git a/bin/Crunch.app/Contents/Resources/crunch.py b/bin/Crunch.app/Contents/Resources/crunch.py index 54cf372..31a74fa 100755 --- a/bin/Crunch.app/Contents/Resources/crunch.py +++ b/bin/Crunch.app/Contents/Resources/crunch.py @@ -16,12 +16,14 @@ import shutil import struct import subprocess +import time from subprocess import CalledProcessError from multiprocessing import Lock, Pool, cpu_count # Locks -lock = Lock() +stdstream_lock = Lock() +logging_lock = Lock() # Processor Constant # - Modify this to an integer value if you want to fix the number of @@ -36,8 +38,14 @@ PNGQUANT_CLI_PATH = os.path.join(os.path.expanduser("~"), "pngquant", "pngquant") ZOPFLIPNG_CLI_PATH = os.path.join(os.path.expanduser("~"), "zopfli", "zopflipng") +# Crunch Directory (dot directory in $HOME) +CRUNCH_DOT_DIRECTORY = os.path.join(os.path.expanduser("~"), ".crunch") + +# Log File Path Constants +LOGFILE_PATH = os.path.join(CRUNCH_DOT_DIRECTORY, "crunch.log") + # Application Constants -VERSION = "2.1.0" +VERSION = "3.0.0" VERSION_STRING = "crunch v" + VERSION HELP_STRING = """ @@ -62,6 +70,15 @@ 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): @@ -80,10 +97,10 @@ def main(argv): # HELP, USAGE, VERSION option handling # ////////////////////////////////////// - if argv[0] == "-v" or argv[0] == "--version": + if argv[0] in ("-v", "--version"): print(VERSION_STRING) sys.exit(0) - elif argv[0] == "-h" or argv[0] == "--help": + elif argv[0] in ("-h", "--help"): print(HELP_STRING) sys.exit(0) elif argv[0] == "--usage": @@ -100,7 +117,7 @@ def main(argv): # PARSE PNG_PATH_LIST # //////////////////// - if argv[0] == "--gui" or argv[0] == "--service": + if is_gui(argv): png_path_list = argv[1:] else: png_path_list = argv @@ -109,20 +126,25 @@ def main(argv): # COMMAND LINE ERROR HANDLING # ////////////////////////////////// + NOTPNG_ERROR_FOUND = False 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.exit(1) + sys.stderr.write("[ERROR] '" + 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.exit(1) + 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) + if is_gui(argv): + 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): @@ -132,6 +154,8 @@ def main(argv): + "'" + os.linesep ) + if is_gui(argv): + 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( @@ -140,6 +164,8 @@ def main(argv): + "'" + os.linesep ) + if is_gui(argv): + log_error("zopflipng was not found on the expected path " + ZOPFLIPNG_EXE_PATH) sys.exit(1) # //////////////////////////////////// @@ -150,7 +176,6 @@ def main(argv): if len(png_path_list) == 1: # there is only one PNG file, skip spawning of processes and just optimize it optimize_png(png_path_list[0]) - sys.exit(0) else: processes = PROCESSES # if not defined by user, start by defining spawned processes as number of available cores @@ -166,13 +191,19 @@ def main(argv): try: p.map(optimize_png, png_path_list) except Exception as e: - lock.acquire() + stdstream_lock.acquire() sys.stderr.write("-----" + os.linesep) sys.stderr.write("[ERROR] Error detected during execution of the request." + os.linesep) sys.stderr.write(str(e) + os.linesep) - lock.release() + stdstream_lock.release() + if is_gui(argv): + log_error(str(e)) sys.exit(1) - sys.exit(0) + + # end of successful processing, exit code 0 + if is_gui(argv): + log_info("Crunch execution ended.") + sys.exit(0) # /////////////////////// @@ -189,9 +220,9 @@ def optimize_png(png_path): # -------------- # 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) try: - pngquant_options = " --quality=80-98 --skip-if-larger --force --ext -crunch.png " - pngquant_command = PNGQUANT_EXE_PATH + pngquant_options + shellquote(img.pre_filepath) subprocess.check_output(pngquant_command, stderr=subprocess.STDOUT, shell=True) except CalledProcessError as cpe: if cpe.returncode == 98: @@ -205,45 +236,67 @@ def optimize_png(png_path): # below if it is not present to these errors pass else: - lock.acquire() + stdstream_lock.acquire() sys.stderr.write("[ERROR] " + img.pre_filepath + " processing failed at the pngquant stage." + os.linesep) - lock.release() - if sys.argv[1] == "--gui" or sys.argv[1] == "--service": + stdstream_lock.release() + if is_gui(sys.argv): + log_error(img.pre_filepath + " processing failed at the pngquant stage. " + os.linesep + str(cpe)) return None else: raise cpe except Exception as e: - raise e + if is_gui(sys.argv): + log_error(img.pre_filepath + " processing failed at the pngquant stage. " + os.linesep + str(e)) + return None + else: + raise e # --------------- # zopflipng stage # --------------- - - # confirm that a file with proper path was generated above (occurs with larger files following pngquant) + # use --filters=0 by default for quantized PNG files (based upon testing by CS) + zopflipng_options = " -y --filters=0 " + # confirm that a file with proper path was generated by pngquant + # pngquant does not write expected file path if the file was larger after processing if not os.path.exists(img.post_filepath): shutil.copy(img.pre_filepath, img.post_filepath) + # If pngquant did not quantize the file, permit zopflipng to attempt compression with mulitple + # 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) try: - zopflipng_options = " -y " - zopflipng_command = ZOPFLIPNG_EXE_PATH + zopflipng_options + shellquote(img.post_filepath) + " " + shellquote(img.post_filepath) subprocess.check_output(zopflipng_command, stderr=subprocess.STDOUT, shell=True) except CalledProcessError as cpe: - lock.acquire() + stdstream_lock.acquire() sys.stderr.write("[ERROR] " + img.pre_filepath + " processing failed at the zopflipng stage." + os.linesep) - lock.release() - if sys.argv[1] == "--gui" or sys.argv[1] == "--service": + stdstream_lock.release() + if is_gui(sys.argv): + log_error(img.pre_filepath + " processing failed at the zopflipng stage. " + os.linesep + str(cpe)) return None else: raise cpe except Exception as e: - raise e + if is_gui(sys.argv): + log_error(img.pre_filepath + " processing failed at the pngquant stage. " + os.linesep + str(e)) + return None + else: + raise e + # 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) - lock.acquire() + # 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)") - lock.release() + stdstream_lock.release() + + # report percent original file size / post file path / size (bytes) to stdout (macOS GUI + right-click service) + if is_gui(sys.argv): + log_info("[ " + percent_string + "% ] " + + img.post_filepath + " (" + str(img.post_size) + " bytes)") def fix_filepath_args(args): @@ -253,7 +306,8 @@ def fix_filepath_args(args): if arg[0] == "-": # add command line options arg_list.append(arg) - elif len(arg) > 4 and arg[-4:] == ".png": + elif len(arg) > 2 and "." in arg[1:]: + # if format is `\w+\.\w+`, then this is a filename, not directory # this is the end of a filepath string that may have had # spaces in directories prior to this level. Let's recreate # the entire original path @@ -287,6 +341,10 @@ def get_zopflipng_path(): return ZOPFLIPNG_CLI_PATH +def is_gui(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) @@ -297,6 +355,27 @@ def is_valid_png(filepath): return signature == expected_signature +def log_error(errmsg): + current_time = time.strftime("%m-%d-%y %H:%M:%S") + logging_lock.acquire() + with open(LOGFILE_PATH, 'a') as filewriter: + filewriter.write(current_time + "\tERROR\t" + errmsg + os.linesep) + filewriter.flush() + os.fsync(filewriter.fileno()) + logging_lock.release() + + +def log_info(infomsg): + current_time = time.strftime("%m-%d-%y %H:%M:%S") + logging_lock.acquire() + with open(LOGFILE_PATH, 'a') as filewriter: + filewriter.write(current_time + "\tINFO\t" + infomsg + os.linesep) + filewriter.flush() + os.fsync(filewriter.fileno()) + logging_lock.release() + return None + + def shellquote(filepath): return "'" + filepath.replace("'", "'\\''") + "'" @@ -336,7 +415,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] == "--gui" or sys.argv[1] == "--service": + if 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/execution.html b/bin/Crunch.app/Contents/Resources/execution.html index 718d27b..bc7a2aa 100644 --- a/bin/Crunch.app/Contents/Resources/execution.html +++ b/bin/Crunch.app/Contents/Resources/execution.html @@ -9,18 +9,13 @@ body, html { overflow: hidden; - background-color: #FFF; + background-color: #424242; } .frame { - height: 200px; white-space: nowrap; text-align: center; } - - img { - margin-top: 50px; - } @@ -28,7 +23,7 @@
- +
diff --git a/bin/Crunch.app/Contents/Resources/fastdots.gif b/bin/Crunch.app/Contents/Resources/fastdots.gif deleted file mode 100644 index 28c6096..0000000 Binary files a/bin/Crunch.app/Contents/Resources/fastdots.gif and /dev/null differ diff --git a/bin/Crunch.app/Contents/Resources/pngquant b/bin/Crunch.app/Contents/Resources/pngquant index cb81892..d8db8d2 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 64d0956..59687c3 100755 --- a/bin/Crunch.app/Contents/Resources/script +++ b/bin/Crunch.app/Contents/Resources/script @@ -17,14 +17,29 @@ # Message on application open (no arguments passed to script on initial open) if [ $# -eq 0 ]; then - cat start.html + cat waiting.html exit 0 fi cat execution.html -./crunch.py --gui "$@" >/dev/null - -cat clear.html -cat complete.html -exit 0 +if ./crunch.py --gui "$@" >/dev/null 2>&1; then + cat clear.html + cat complete-success.html + sleep 2 + cat clear.html + cat start.html + sleep 0.8 + cat waiting.html + exit 0 +else + sleep 0.6 + cat clear.html + cat complete-error.html + sleep 2 + cat clear.html + cat start.html + sleep 0.8 + cat waiting.html + exit 1 +fi diff --git a/bin/Crunch.app/Contents/Resources/slowdots.gif b/bin/Crunch.app/Contents/Resources/slowdots.gif deleted file mode 100644 index fbb3a67..0000000 Binary files a/bin/Crunch.app/Contents/Resources/slowdots.gif and /dev/null differ diff --git a/bin/Crunch.app/Contents/Resources/start.html b/bin/Crunch.app/Contents/Resources/start.html index 661cd37..8582f5f 100644 --- a/bin/Crunch.app/Contents/Resources/start.html +++ b/bin/Crunch.app/Contents/Resources/start.html @@ -4,30 +4,24 @@ Start -
- +
diff --git a/bin/Crunch.app/Contents/Resources/waiting.html b/bin/Crunch.app/Contents/Resources/waiting.html new file mode 100644 index 0000000..b5c6221 --- /dev/null +++ b/bin/Crunch.app/Contents/Resources/waiting.html @@ -0,0 +1,27 @@ + + + + + + Start + + + + + +
+ +
+ + + \ No newline at end of file diff --git a/bin/Crunch.app/Contents/Resources/zopflipng b/bin/Crunch.app/Contents/Resources/zopflipng index 57f2a21..f1caf2e 100755 Binary files a/bin/Crunch.app/Contents/Resources/zopflipng and b/bin/Crunch.app/Contents/Resources/zopflipng differ diff --git a/docs/Credits.html b/docs/Credits.html index dbf82b2..9d89ddf 100644 --- a/docs/Credits.html +++ b/docs/Credits.html @@ -26,7 +26,7 @@

Upgrade

To upgrade with Homebrew, enter the following command in your terminal:

- $ brew cask uninstall crunch && brew cask install crunch + $ brew cask upgrade

Other Crunch Tools

@@ -57,4 +57,8 @@

Embedded Software Licenses

+

+

Animations

+

The Crunch GUI animations were designed by + Gary Jacobs and are licensed under the MIT License.

\ No newline at end of file diff --git a/docs/EXECUTABLE.md b/docs/EXECUTABLE.md index 8c4bb33..267d40f 100644 --- a/docs/EXECUTABLE.md +++ b/docs/EXECUTABLE.md @@ -5,6 +5,7 @@ The `crunch` command line executable is a *nix executable that supports parallel ## Contents - [Install documentation](#install) +- [Upgrade documentation](#upgrade) - [Usage documentation](#usage) ## What Happens During the Installation? @@ -17,7 +18,7 @@ There is a method to the madness of the install paths. `crunch` is installed on $ crunch myimage.png ``` -The `pngquant` and `zopflipng` executables are installed off of your system PATH (in subdirectories of your $HOME directory) in order to pin the versions of the applications to the same git commits that are distributed with the rest of the Crunch project tools. These may or may not be the most current releases of the two project dependencies. Maintaining an always current dependency state is less important to me than that they are tested as part of this project and will allow you to reproduce the same optimized images irrespective of the Crunch tool that you choose to use (and that the data that are displayed on the repository are valid across all of the tools). The off system PATH install approach for the project dependencies also provides you with the option to install different versions of `pngquant` and `zopflipng` on your system PATH (e.g., with a package manager) should you want to use different versions on the command line. Feel free to file an issue report if you disagree with these decisions. +The `pngquant` and `zopflipng` executables are installed off of your system PATH (in subdirectories of your $HOME directory) in order to pin the versions of the applications to the same git commits that are distributed with the rest of the Crunch project tools. These may or may not be the most current releases of the two project dependencies. Maintaining an always current dependency state is less important to me than that they are tested as part of this project and will allow you to reproduce the same optimized images irrespective of the Crunch tool that you choose to use (and that the data that are displayed on the repository are valid across all of the tools). The off system PATH install approach for the project dependencies also provides you with the option to install different versions of `pngquant` and `zopflipng` on your system PATH (e.g., with a package manager) should you want to use different versions on the command line. ## Install @@ -59,6 +60,31 @@ $ crunch --help This command should display the in-application help message for the `crunch` executable. If you see this text, you are all set to use `crunch`. +## Upgrade + +Upgrade the `crunch` executable by cloning the git repository, rebuilding the `pngquant` and `zopflipng` dependencies, and installing the `crunch` executable file. + +You can perform these steps with the following set of commands: + +``` +$ git clone https://github.com/chrissimpkins/Crunch.git +$ cd Crunch +$ rm -rf ~/pngquant +$ rm -rf ~/zopfli +$ make build-dependencies +$ make install-executable +``` + +You will be prompted for your password with the final command above. Enter it and confirm the install by entering the following command: + +``` +$ crunch --version +``` + +You should see the expected version of the `crunch` tool installed on your system. You may delete the entire Crunch git repository after you complete these steps (or save it and pull new changes from the master branch to build the next version if you plan to stay current with releases). New releases are indicated with git tags. + + + ## Usage Image processing is executed by requesting one or more PNG image paths as arguments to the `crunch` executable: diff --git a/docs/MACOSGUI.md b/docs/MACOSGUI.md index ff86907..d2d3a10 100644 --- a/docs/MACOSGUI.md +++ b/docs/MACOSGUI.md @@ -37,7 +37,7 @@ Upgrade by following the same instructions and allowing the new version to repla Drag and drop your PNG images onto the Crunch window: -Crunch PNG image optimization usage +Crunch PNG image optimization usage Optimized files are saved in the same directory as the original with the modified path `[original filename]-crunch.png`. diff --git a/html/clear.html b/html/clear.html index 3e0651f..fb80407 100644 --- a/html/clear.html +++ b/html/clear.html @@ -9,6 +9,7 @@ body, html { overflow: hidden; + background-color: #424242; } .clear { diff --git a/html/complete-error.html b/html/complete-error.html new file mode 100644 index 0000000..a6a7c1c --- /dev/null +++ b/html/complete-error.html @@ -0,0 +1,30 @@ + + + + + + Start + + + + + + +
+ + +
+ + + \ No newline at end of file diff --git a/html/complete-success.html b/html/complete-success.html new file mode 100644 index 0000000..6f2643d --- /dev/null +++ b/html/complete-success.html @@ -0,0 +1,30 @@ + + + + + + Start + + + + + + +
+ + +
+ + + \ No newline at end of file diff --git a/html/execution.html b/html/execution.html index 718d27b..bc7a2aa 100644 --- a/html/execution.html +++ b/html/execution.html @@ -9,18 +9,13 @@ body, html { overflow: hidden; - background-color: #FFF; + background-color: #424242; } .frame { - height: 200px; white-space: nowrap; text-align: center; } - - img { - margin-top: 50px; - } @@ -28,7 +23,7 @@
- +
diff --git a/html/start.html b/html/start.html index 661cd37..8582f5f 100644 --- a/html/start.html +++ b/html/start.html @@ -4,30 +4,24 @@ Start -
- +
diff --git a/html/waiting.html b/html/waiting.html new file mode 100644 index 0000000..b5c6221 --- /dev/null +++ b/html/waiting.html @@ -0,0 +1,27 @@ + + + + + + Start + + + + + +
+ +
+ + + \ No newline at end of file diff --git a/img/animations/01-Load-Up@1x.gif b/img/animations/01-Load-Up@1x.gif new file mode 100644 index 0000000..4c2d9a6 Binary files /dev/null and b/img/animations/01-Load-Up@1x.gif differ diff --git a/img/animations/01-Load-Up@2x.gif b/img/animations/01-Load-Up@2x.gif new file mode 100644 index 0000000..85ef2e2 Binary files /dev/null and b/img/animations/01-Load-Up@2x.gif differ diff --git a/img/animations/02-Waiting@1x.gif b/img/animations/02-Waiting@1x.gif new file mode 100644 index 0000000..82fe999 Binary files /dev/null and b/img/animations/02-Waiting@1x.gif differ diff --git a/img/animations/02-Waiting@2x.gif b/img/animations/02-Waiting@2x.gif new file mode 100644 index 0000000..2b8da05 Binary files /dev/null and b/img/animations/02-Waiting@2x.gif differ diff --git a/img/animations/03-Crunching@1x.gif b/img/animations/03-Crunching@1x.gif new file mode 100644 index 0000000..daab318 Binary files /dev/null and b/img/animations/03-Crunching@1x.gif differ diff --git a/img/animations/03-Crunching@2x.gif b/img/animations/03-Crunching@2x.gif new file mode 100644 index 0000000..0f73546 Binary files /dev/null and b/img/animations/03-Crunching@2x.gif differ diff --git a/img/animations/04-Error@1x.gif b/img/animations/04-Error@1x.gif new file mode 100644 index 0000000..39cc93b Binary files /dev/null and b/img/animations/04-Error@1x.gif differ diff --git a/img/animations/04-Error@2x.gif b/img/animations/04-Error@2x.gif new file mode 100644 index 0000000..78a9816 Binary files /dev/null and b/img/animations/04-Error@2x.gif differ diff --git a/img/animations/04-Success@1x.gif b/img/animations/04-Success@1x.gif new file mode 100644 index 0000000..c63e7bd Binary files /dev/null and b/img/animations/04-Success@1x.gif differ diff --git a/img/animations/04-Success@2x.gif b/img/animations/04-Success@2x.gif new file mode 100644 index 0000000..cc7d0cb Binary files /dev/null and b/img/animations/04-Success@2x.gif differ diff --git a/img/cat-1285634_640-crunch.png b/img/cat-1285634_640-crunch.png index f31f168..10eea28 100644 Binary files a/img/cat-1285634_640-crunch.png and b/img/cat-1285634_640-crunch.png differ diff --git a/img/colors-157474_640-crunch.png b/img/colors-157474_640-crunch.png index 9a656cc..98da4d5 100644 Binary files a/img/colors-157474_640-crunch.png and b/img/colors-157474_640-crunch.png differ diff --git a/img/crunch-ss-2.gif b/img/crunch-ss-2.gif new file mode 100644 index 0000000..3522750 Binary files /dev/null and b/img/crunch-ss-2.gif differ diff --git a/img/crunch-ss.gif b/img/crunch-ss.gif deleted file mode 100644 index f58b6a8..0000000 Binary files a/img/crunch-ss.gif and /dev/null differ diff --git a/img/flowers-67839_640-crunch.png b/img/flowers-67839_640-crunch.png index a1e1f83..544a5b8 100644 Binary files a/img/flowers-67839_640-crunch.png and b/img/flowers-67839_640-crunch.png differ diff --git a/img/prairie-679014_640-crunch.png b/img/prairie-679014_640-crunch.png index fa77770..e591770 100644 Binary files a/img/prairie-679014_640-crunch.png and b/img/prairie-679014_640-crunch.png differ diff --git a/img/robot-1214536_640-crunch.png b/img/robot-1214536_640-crunch.png index a1d035f..638b283 100644 Binary files a/img/robot-1214536_640-crunch.png and b/img/robot-1214536_640-crunch.png differ diff --git a/img/suns-rays-478249_640-crunch.png b/img/suns-rays-478249_640-crunch.png index 36d4497..bdadd35 100644 Binary files a/img/suns-rays-478249_640-crunch.png and b/img/suns-rays-478249_640-crunch.png differ diff --git a/installer/Crunch-Installer-checksum.txt b/installer/Crunch-Installer-checksum.txt index 3a11299..21e7382 100644 --- a/installer/Crunch-Installer-checksum.txt +++ b/installer/Crunch-Installer-checksum.txt @@ -1 +1 @@ -8692fb7e151321a60f7528a4fc31e9dd7a4b1ae1 Crunch-Installer.dmg +100f07220c3991fc06f41bf58e4685a0bc659b44 Crunch-Installer.dmg diff --git a/installer/Crunch-Installer.dmg b/installer/Crunch-Installer.dmg index 5cff3f6..6dc2c29 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 e6f5d25..8f3365d 100644 --- a/profile/Crunch.platypus +++ b/profile/Crunch.platypus @@ -13,16 +13,21 @@ BundledFiles /Users/ces/Desktop/code/Crunch/docs/Credits.html - /Users/ces/Desktop/code/Crunch/html/complete.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/img/fastdots.gif - /Users/ces/Desktop/code/Crunch/img/slowdots.gif /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 Creator Platypus-5.2 @@ -265,6 +270,6 @@ UseXMLPlistFormat Version - 2.0.0 + 3.0.0-dev2 diff --git a/src/crunch-gui.sh b/src/crunch-gui.sh index 64d0956..59687c3 100755 --- a/src/crunch-gui.sh +++ b/src/crunch-gui.sh @@ -17,14 +17,29 @@ # Message on application open (no arguments passed to script on initial open) if [ $# -eq 0 ]; then - cat start.html + cat waiting.html exit 0 fi cat execution.html -./crunch.py --gui "$@" >/dev/null - -cat clear.html -cat complete.html -exit 0 +if ./crunch.py --gui "$@" >/dev/null 2>&1; then + cat clear.html + cat complete-success.html + sleep 2 + cat clear.html + cat start.html + sleep 0.8 + cat waiting.html + exit 0 +else + sleep 0.6 + cat clear.html + cat complete-error.html + sleep 2 + cat clear.html + cat start.html + sleep 0.8 + cat waiting.html + exit 1 +fi diff --git a/src/crunch.py b/src/crunch.py index 54cf372..31a74fa 100755 --- a/src/crunch.py +++ b/src/crunch.py @@ -16,12 +16,14 @@ import shutil import struct import subprocess +import time from subprocess import CalledProcessError from multiprocessing import Lock, Pool, cpu_count # Locks -lock = Lock() +stdstream_lock = Lock() +logging_lock = Lock() # Processor Constant # - Modify this to an integer value if you want to fix the number of @@ -36,8 +38,14 @@ PNGQUANT_CLI_PATH = os.path.join(os.path.expanduser("~"), "pngquant", "pngquant") ZOPFLIPNG_CLI_PATH = os.path.join(os.path.expanduser("~"), "zopfli", "zopflipng") +# Crunch Directory (dot directory in $HOME) +CRUNCH_DOT_DIRECTORY = os.path.join(os.path.expanduser("~"), ".crunch") + +# Log File Path Constants +LOGFILE_PATH = os.path.join(CRUNCH_DOT_DIRECTORY, "crunch.log") + # Application Constants -VERSION = "2.1.0" +VERSION = "3.0.0" VERSION_STRING = "crunch v" + VERSION HELP_STRING = """ @@ -62,6 +70,15 @@ 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): @@ -80,10 +97,10 @@ def main(argv): # HELP, USAGE, VERSION option handling # ////////////////////////////////////// - if argv[0] == "-v" or argv[0] == "--version": + if argv[0] in ("-v", "--version"): print(VERSION_STRING) sys.exit(0) - elif argv[0] == "-h" or argv[0] == "--help": + elif argv[0] in ("-h", "--help"): print(HELP_STRING) sys.exit(0) elif argv[0] == "--usage": @@ -100,7 +117,7 @@ def main(argv): # PARSE PNG_PATH_LIST # //////////////////// - if argv[0] == "--gui" or argv[0] == "--service": + if is_gui(argv): png_path_list = argv[1:] else: png_path_list = argv @@ -109,20 +126,25 @@ def main(argv): # COMMAND LINE ERROR HANDLING # ////////////////////////////////// + NOTPNG_ERROR_FOUND = False 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.exit(1) + sys.stderr.write("[ERROR] '" + 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.exit(1) + 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) + if is_gui(argv): + 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): @@ -132,6 +154,8 @@ def main(argv): + "'" + os.linesep ) + if is_gui(argv): + 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( @@ -140,6 +164,8 @@ def main(argv): + "'" + os.linesep ) + if is_gui(argv): + log_error("zopflipng was not found on the expected path " + ZOPFLIPNG_EXE_PATH) sys.exit(1) # //////////////////////////////////// @@ -150,7 +176,6 @@ def main(argv): if len(png_path_list) == 1: # there is only one PNG file, skip spawning of processes and just optimize it optimize_png(png_path_list[0]) - sys.exit(0) else: processes = PROCESSES # if not defined by user, start by defining spawned processes as number of available cores @@ -166,13 +191,19 @@ def main(argv): try: p.map(optimize_png, png_path_list) except Exception as e: - lock.acquire() + stdstream_lock.acquire() sys.stderr.write("-----" + os.linesep) sys.stderr.write("[ERROR] Error detected during execution of the request." + os.linesep) sys.stderr.write(str(e) + os.linesep) - lock.release() + stdstream_lock.release() + if is_gui(argv): + log_error(str(e)) sys.exit(1) - sys.exit(0) + + # end of successful processing, exit code 0 + if is_gui(argv): + log_info("Crunch execution ended.") + sys.exit(0) # /////////////////////// @@ -189,9 +220,9 @@ def optimize_png(png_path): # -------------- # 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) try: - pngquant_options = " --quality=80-98 --skip-if-larger --force --ext -crunch.png " - pngquant_command = PNGQUANT_EXE_PATH + pngquant_options + shellquote(img.pre_filepath) subprocess.check_output(pngquant_command, stderr=subprocess.STDOUT, shell=True) except CalledProcessError as cpe: if cpe.returncode == 98: @@ -205,45 +236,67 @@ def optimize_png(png_path): # below if it is not present to these errors pass else: - lock.acquire() + stdstream_lock.acquire() sys.stderr.write("[ERROR] " + img.pre_filepath + " processing failed at the pngquant stage." + os.linesep) - lock.release() - if sys.argv[1] == "--gui" or sys.argv[1] == "--service": + stdstream_lock.release() + if is_gui(sys.argv): + log_error(img.pre_filepath + " processing failed at the pngquant stage. " + os.linesep + str(cpe)) return None else: raise cpe except Exception as e: - raise e + if is_gui(sys.argv): + log_error(img.pre_filepath + " processing failed at the pngquant stage. " + os.linesep + str(e)) + return None + else: + raise e # --------------- # zopflipng stage # --------------- - - # confirm that a file with proper path was generated above (occurs with larger files following pngquant) + # use --filters=0 by default for quantized PNG files (based upon testing by CS) + zopflipng_options = " -y --filters=0 " + # confirm that a file with proper path was generated by pngquant + # pngquant does not write expected file path if the file was larger after processing if not os.path.exists(img.post_filepath): shutil.copy(img.pre_filepath, img.post_filepath) + # If pngquant did not quantize the file, permit zopflipng to attempt compression with mulitple + # 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) try: - zopflipng_options = " -y " - zopflipng_command = ZOPFLIPNG_EXE_PATH + zopflipng_options + shellquote(img.post_filepath) + " " + shellquote(img.post_filepath) subprocess.check_output(zopflipng_command, stderr=subprocess.STDOUT, shell=True) except CalledProcessError as cpe: - lock.acquire() + stdstream_lock.acquire() sys.stderr.write("[ERROR] " + img.pre_filepath + " processing failed at the zopflipng stage." + os.linesep) - lock.release() - if sys.argv[1] == "--gui" or sys.argv[1] == "--service": + stdstream_lock.release() + if is_gui(sys.argv): + log_error(img.pre_filepath + " processing failed at the zopflipng stage. " + os.linesep + str(cpe)) return None else: raise cpe except Exception as e: - raise e + if is_gui(sys.argv): + log_error(img.pre_filepath + " processing failed at the pngquant stage. " + os.linesep + str(e)) + return None + else: + raise e + # 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) - lock.acquire() + # 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)") - lock.release() + stdstream_lock.release() + + # report percent original file size / post file path / size (bytes) to stdout (macOS GUI + right-click service) + if is_gui(sys.argv): + log_info("[ " + percent_string + "% ] " + + img.post_filepath + " (" + str(img.post_size) + " bytes)") def fix_filepath_args(args): @@ -253,7 +306,8 @@ def fix_filepath_args(args): if arg[0] == "-": # add command line options arg_list.append(arg) - elif len(arg) > 4 and arg[-4:] == ".png": + elif len(arg) > 2 and "." in arg[1:]: + # if format is `\w+\.\w+`, then this is a filename, not directory # this is the end of a filepath string that may have had # spaces in directories prior to this level. Let's recreate # the entire original path @@ -287,6 +341,10 @@ def get_zopflipng_path(): return ZOPFLIPNG_CLI_PATH +def is_gui(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) @@ -297,6 +355,27 @@ def is_valid_png(filepath): return signature == expected_signature +def log_error(errmsg): + current_time = time.strftime("%m-%d-%y %H:%M:%S") + logging_lock.acquire() + with open(LOGFILE_PATH, 'a') as filewriter: + filewriter.write(current_time + "\tERROR\t" + errmsg + os.linesep) + filewriter.flush() + os.fsync(filewriter.fileno()) + logging_lock.release() + + +def log_info(infomsg): + current_time = time.strftime("%m-%d-%y %H:%M:%S") + logging_lock.acquire() + with open(LOGFILE_PATH, 'a') as filewriter: + filewriter.write(current_time + "\tINFO\t" + infomsg + os.linesep) + filewriter.flush() + os.fsync(filewriter.fileno()) + logging_lock.release() + return None + + def shellquote(filepath): return "'" + filepath.replace("'", "'\\''") + "'" @@ -336,7 +415,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] == "--gui" or sys.argv[1] == "--service": + if 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 cb81892..d8db8d2 100755 Binary files a/src/include/pngquant and b/src/include/pngquant differ diff --git a/src/include/zopflipng b/src/include/zopflipng index 57f2a21..f1caf2e 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 567b2f2..001126f 100755 --- a/src/install-dependencies.sh +++ b/src/install-dependencies.sh @@ -15,8 +15,8 @@ PNGQUANT_EXE="$PNGQUANT_BUILD_DIR/pngquant" ZOPFLIPNG_BUILD_DIR="$HOME/zopfli" ZOPFLIPNG_EXE="$ZOPFLIPNG_BUILD_DIR/zopflipng" -PNGQUANT_VERSION_TAG="2.11.7" -ZOPFLIPNG_VERSION_TAG="zopfli-1.0.1" +PNGQUANT_VERSION_TAG="2.12.0" +ZOPFLIPNG_VERSION_TAG="v2.1.0" LIBPNG_VERSION="1.6.34" LIBPNG_VERSION_DOWNLOAD="libpng16/$LIBPNG_VERSION/libpng-$LIBPNG_VERSION.tar.xz" LITTLECMS_VERSION="2.9" @@ -37,6 +37,7 @@ cd "$HOME" || exit 1 git clone --recursive https://github.com/kornelski/pngquant.git cd "$PNGQUANT_BUILD_DIR" || exit 1 git checkout $PNGQUANT_VERSION_TAG +git submodule update # Clone libpng source as a subdirectory of pngquant source (as per pngquant static compilation documentation) curl -L -O "https://sourceforge.net/projects/libpng/files/$LIBPNG_VERSION_DOWNLOAD" @@ -72,7 +73,7 @@ fi cd "$HOME" || exit 1 -git clone https://github.com/google/zopfli.git +git clone https://github.com/chrissimpkins/zopfli.git cd zopfli || exit 1 git checkout "$ZOPFLIPNG_VERSION_TAG" diff --git a/src/test_crunch_errors.py b/src/test_crunch_errors.py index 91f21a2..86bf227 100644 --- a/src/test_crunch_errors.py +++ b/src/test_crunch_errors.py @@ -1,6 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +import os import sys import pytest @@ -55,4 +56,53 @@ def test_crunch_bad_filepath_error(capsys): out, err = capsys.readouterr() assert len(err) > 0 - assert err.startswith("[ERROR]") is True \ No newline at end of file + assert err.startswith("[ERROR]") is True + + +# /////////////////////////////////////////////////////// +# +# Missing dependency error tests +# +# /////////////////////////////////////////////////////// + +def test_crunch_missing_pngquant_error(capsys, monkeypatch): + 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): + src.crunch.main([testpath]) + + out, err = capsys.readouterr() + assert err.startswith("[ERROR]") is True + + +def test_crunch_missing_zopflipng_error(capsys, monkeypatch): + 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): + src.crunch.main([testpath]) + + out, err = capsys.readouterr() + assert err.startswith("[ERROR]") is True + + +# /////////////////////////////////////////////////////// +# +# Multiprocessing.Pool error tests +# +# /////////////////////////////////////////////////////// + +def test_crunch_exception_multiprocessing_pool(capsys, monkeypatch): + def raise_ioerror(): + 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): + src.crunch.main([testpath1, testpath2]) + + out, err = capsys.readouterr() + assert "[ERROR]" in err diff --git a/src/test_crunch_execution.py b/src/test_crunch_execution.py index b00fc5d..2660d4b 100644 --- a/src/test_crunch_execution.py +++ b/src/test_crunch_execution.py @@ -5,6 +5,7 @@ import sys import platform import pytest +import shutil from subprocess import CalledProcessError import src.crunch @@ -222,6 +223,15 @@ def test_crunch_function_fix_filepath_args_twopng_withdir_withmultispace_withopt assert response[2] == "dir nspace/dir2 nspace/test2 img.png" +def test_crunch_function_fix_filepath_args_two_nonpng_files(): + testargs = ["--option", "dir", "nspace/dir1", "nspace/test", "img.html", "dir", "nspace/dir2", "nspace/test2", "img.html"] + response = src.crunch.fix_filepath_args(testargs) + assert len(response) == 3 + assert response[0] == "--option" + assert response[1] == "dir nspace/dir1 nspace/test img.html" + assert response[2] == "dir nspace/dir2 nspace/test2 img.html" + + # optimize_png function def test_crunch_function_optimize_png_unoptimized_file(): @@ -310,6 +320,7 @@ def test_crunch_function_main_multi_file(): 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") @@ -326,8 +337,11 @@ def test_crunch_function_main_single_file_with_gui_flag(): if os.path.exists(testpath): os.remove(testpath) + teardown_logging_path() + 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") @@ -343,9 +357,12 @@ def test_crunch_function_main_single_file_with_service_flag(): # cleanup optimized file produced by this test if os.path.exists(testpath): os.remove(testpath) + + teardown_logging_path() 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") @@ -371,8 +388,11 @@ def test_crunch_function_main_multi_file_with_gui_flag(): if os.path.exists(testpath2): os.remove(testpath2) + teardown_logging_path() + 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") @@ -397,3 +417,55 @@ def test_crunch_function_main_multi_file_with_service_flag(): os.remove(testpath1) if os.path.exists(testpath2): os.remove(testpath2) + + teardown_logging_path() + + +# ////////////////////////////// +# Logging tests +# ////////////////////////////// + +def test_crunch_log_error(): + setup_logging_path() + + logpath = src.crunch.LOGFILE_PATH + src.crunch.log_error("This is a test error message") + assert os.path.isfile(logpath) + freader = open(logpath, 'r') + text = freader.read() + assert "ERROR" in text + assert "This is a test error message" in text + freader.close() + + teardown_logging_path() + + +def test_crunch_log_info(): + setup_logging_path() + + logpath = src.crunch.LOGFILE_PATH + src.crunch.log_info("This is a test info message") + assert os.path.isfile(logpath) + freader = open(logpath, 'r') + text = freader.read() + assert "INFO" in text + assert "This is a test info message" in text + freader.close() + + teardown_logging_path() + + +# Utility functions + +def setup_logging_path(): + # setup the logging directory + if not os.path.isdir(src.crunch.CRUNCH_DOT_DIRECTORY): + os.makedirs(src.crunch.CRUNCH_DOT_DIRECTORY) + # setup the log file + if not os.path.isfile(src.crunch.LOGFILE_PATH): + open(src.crunch.LOGFILE_PATH, "w").close() + + +def teardown_logging_path(): + if os.path.isfile(src.crunch.LOGFILE_PATH): + shutil.rmtree(os.path.join(os.path.expanduser("~"), ".crunch")) diff --git a/src/test_crunch_obj.py b/src/test_crunch_obj.py index f06146b..1349f28 100644 --- a/src/test_crunch_obj.py +++ b/src/test_crunch_obj.py @@ -17,7 +17,7 @@ def test_crunch_imagefile_obj_instantiation(): def test_crunch_imagefile_obj_get_post_filesize_method(): imgfile = ImageFile(os.path.join("img", "robot-1214536_640.png")) imgfile.get_post_filesize() - assert imgfile.post_size == 67773 + assert imgfile.post_size == 67596 def test_crunch_imagefile_obj_get_compression_percent_method():