[
  {
    "path": "LICENSE",
    "content": "This software is published under the BSD licence 3.0\n\nCopyright (c) 2013-2019, Tobias Baldauf All rights reserved.\n\nMail: kontakt@tobias-baldauf.de\nWeb: http://who.tobias.is/\nTwitter: @tbaldauf\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n * Redistributions of source code must retain the above copyright notice,\n this list of conditions and the following disclaimer.\n\n * Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in the\n documentation and/or other materials provided with the distribution.\n\n * Neither the name of the author nor the names of contributors may be\n used to endorse or promote products derived from this software without\n specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nHOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED\nTO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "README.md",
    "content": "Adept - the adaptive JPG Compressor\n====================\n\n## Quick Start\n\n* Please install [a MSS saliency algorithm binary](http://github.com/technopagan/mss-saliency), [ImageMagick](http://www.imagemagick.org/), [MozJPEG](https://github.com/mozilla/mozjpeg) and [JPEGrescan](https://github.com/kud/jpegrescan)\n* Make MozJPEG available as \"mozjpeg\" in your $PATH, e.g. via symlink\n* Fetch a copy of [adept](https://raw.github.com/technopagan/adept-jpg-compressor/master/adept.sh) and place it somewhere you deem a good place for 3rd party shellscripts, e.g. \"/usr/local/bin\". Make sure the location is in the $PATH of the user(s) who will run adept and ensure that the script is executable (chmod -x).\n* Symlink it as \"adept\" for your own convenience\n* Now you can run \"adept /path/to/image.jpg\" to compress JPEGs far more effectively!\n\n## Introduction\n\nWhen compressing JPEG images, the same compression level is used on the entire image. However, most JPEG images contain homogeneous and heterogeneous areas, which are varyingly well-suited for compression. Compressing heterogeneous areas in JPEGs to reduce filesize causes [compression artefacts](https://en.wikipedia.org/wiki/Compression_artifact) due to the lossy nature of JPEG compression.\n\nThis script adaptively alters the compression level for areas within JPEGs (per-block) to achieve optimized file size while maintaining a decent visual quality. This script achieves a significantly reduced file size compared to standard tools such as cjpeg while maintaining good visual quality, as can be measured via SSIM. This is good news for the [Web Performance](https://twitter.com/search?q=%23WebPerf&src=typd) and thus Web Developer community to achieve a great user experience on websites.\n\n## Example\n\nBest quality/size:\n\n```\n$ convert -verbose -quality 100 images/lena.png images/lena.q100.jpg\nimages/lena.png PNG 512x512 512x512+0+0 8-bit DirectClass 475KB 0.010u 0:00.010\nimages/lena.png=>images/lena.q100.jpg PNG 512x512 512x512+0+0 8-bit DirectClass 401KB 0.030u 0:00.039\n\n$ ./adept-jpeg.sh images/lena.q100.jpg\n./adept-jpeg.sh options: inherit, 69, autodetect, _adept_compress_imagemagick\n404266 images/lena.q100.jpg\nsize=8 512 512\ntilecount=64x64\nsalient=1.04084\nsalient=74.321\nsalient=48.2852\nsalient=36.5063\nthreshold=43\nslice to ram... ok.\nestimate content complexity and compress... 4096 tile ok.\nfinal image... ok.\n200860 images/lena.q100_adept_compress_imagemagick.jpg (50%)\n\n$ dssim -o images/lena.q100_adept_compress_imagemagick.c.png images/lena.png images/lena.q100_adept_compress_imagemagick.jpg\n0.00236562  lena.q100_adept_compress_imagemagick.jpg\n```\n![PNG](./images/lena.png) ![JPEG Q=100](./images/lena.q100.jpg) ![ADEPT JPEG](./images/lena.q100_adept_compress_imagemagick.jpg) ![DSSIM](./images/lena.q100_adept_compress_imagemagick.c.png)\n\nPNG -> JPEG Q=100 -> ADEPT JPEG -> Compare (DSSIM)\n\n## Contributors\n\nIn alphabetical order:\n\n * [Andy Davies](http://twitter.com/andydavies)\n * [Gregor Fabritius](http://twitter.com/grefab)\n * [Neil Jedrzejewski](http://www.wunderboy.org/about.php)\n * [Alessandro Lenzen](http://twitter.com/adelnorsz)\n * [Claus Meteling](http://www.xing.com/profile/Claus_Meteling)\n * [André Roaldseth](http://twitter.com/androa)\n * [Christian Schäfer](http://twitter.com/derSchepp)\n * [Yoav Weiss](http://twitter.com/yoavweiss)\n\n## Licence\n\nThis software is published under the BSD licence 3.0\n\nCopyright (c) 2015, Tobias Baldauf\nAll rights reserved.\n\nMail: [kontakt@tobias-baldauf.de](mailto:kontakt@tobias-baldauf.de)\nWeb: [who.tobias.is](http://who.tobias.is/)\nTwitter: [@tbaldauf](http://twitter.com/tbaldauf)\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\n * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\n * Neither the name of the author nor the names of contributors may be used to endorse or promote products derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "adept-jpeg.sh",
    "content": "#!/usr/bin/env bash\n\n###############################################################################\n#\n# Bash script to automate adaptive JPEG compression using common CLI tools\n#\n# Usage: bash adept.sh /path/to/image.jpg\n#\n###############################################################################\n#\n# Brief overview of the mode of operation:\n#\n# The input JPG gets sliced into tiles, sized as a multiple of 8 due to the\n# nature of JPG compression. The image is also run through a saliency\n# detection algorithm and its resulting output further reduced to a\n# 2-color black+white PNG.\n#\n# This bi-color PNG is ideal to measure tiles' gray channel mean value and use\n# it as a single integer indicator to judge its perceivable complexity.\n#\n# Areas with low complexity contents are then exposed to heavier compression.\n# At reassemlby, this leads to savings in image bytesize while maintaining\n# good visual quality because no compression artefacts occur in areas of\n# high-complexity or sharp contrasts.\n#\n###############################################################################\n# Tools that need to be pre-installed:\n#\n#\t* Maximum Symmetric Surround Saliency Algorithm Binary\n#\t http://github.com/technopagan/mss-saliency\n#\n#\t* ImageMagick >= v.6.6\n#\n#\t* MozJPEG\n#\t http://github.com/mozilla/mozjpeg\n#\t Expects Mozjpeg to be available under 'mozjpeg', e.g. via symlink\n#\n#\t* JPEGRescan Perl Script for lossless JPG compression\n#\t http://github.com/kud/jpegrescan\n#\n# Note: Additonal tools are required to run Adept, such as \"bc\",\n# \"find\", \"mv\", \"rm\" and Bash 3.x. As all of these tools are provided by lsbcore, core-utils\n# or similar default packages, we can expect them to be always available.\n#\n###############################################################################\n#\n# This software is published under the BSD licence 3.0\n#\n# Copyright (c) 2013-2015, Tobias Baldauf\n# All rights reserved.\n#\n# Mail: kontakt@tobias-baldauf.de\n# Web: http://who.tobias.is/\n# Twitter: @tbaldauf\n#\n# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n#\n#\t* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n#\n#\t* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\n#\n#\t* Neither the name of the author nor the names of contributors may be used to endorse or promote products derived from this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n#\n###############################################################################\n\nHELP=\"adept-jpeg.sh is a Bash script to automate adaptive JPEG compression using common CLI tools.\n\nUSAGE:\n   $0 [options] path/to/file.jpg\n\nOPTIONS:\n   -h           Displays this help message.\n   -c string    Name compressor: imagemagick (default), cjpeg, jpegoptim, jpge, mozcjpeg.\n   -q 0..100    Compression rate of areas considered as important, default value is 'inherit'.\n   -Q 0..100    Compression rate of areas deemed suitable for high compression, default is 69.\n   -s suffix    This will append 'suffix' to filenames. Default value is '_adept_compress'.\n   -t integer   tile size, accepted values are 8,16,32,64,128 and 256. The smaller the value, the better results\n                but might take signifficantly more time to process. The default value is 'autodetect'.\n\"\n\n###############################################################################\n# USER CONFIGURABLE PARAMETERS\n###############################################################################\n\n# Default JPG quality setting, either inherited or defined as an integer of 0-100\n# Default: inherit\nDEFAULTCOMPRESSIONRATE=\"inherit\"\n\n# JPEG quality setting for areas of the image deemed suitable for high compression in an integer of 0-100\n# Default: 69\nHIGHCOMPRESSIONRATE=\"69\"\n\n# Suffix string to attach to the output JPG filename, e.g. '_adept_compress'\n# If deliberatly set empty (''), the input JPG will be replaced with the new compressed JPG\nOUTPUTFILESUFFIXBEGIN=\"_adept_compress\"\n\n# Square dimensions for all temporary tiles. Tile size heavily influences compression efficiency at the cost of runtime performance\n# E.g. a tile size of 8 yields maximum compression results while taking several minutes of runtime\n# If you chose to manually adjust tile size, only use multiples of 8 (8/16/32/64/128/256)\n# Default: autodetect\nTILESIZE=\"autodetect\"\n\n# Jpeg compressor\n# Default: imagemagick\nJPEGCNAME=\"imagemagick\"\n\n###############################################################################\n# READ COMMAND LINE USER PARAMETERS\n###############################################################################\n\nwhile getopts \":c:q:Q:s:t:h\" opt\ndo\n\tcase \"$opt\" in\n\t\tc)  JPEGCNAME=$OPTARG\n\t\t\t;;\n\t\tq)  DEFAULTCOMPRESSIONRATE=$OPTARG\n\t\t\t;;\n\t\tQ)  HIGHCOMPRESSIONRATE=$OPTARG\n\t\t\t;;\n\t\ts)  OUTPUTFILESUFFIXBEGIN=$OPTARG\n\t\t\t;;\n\t\tt)  TILESIZE=$OPTARG\n\t\t\t;;\n\t\th)  echo \"${HELP}\"\n\t\t\texit 0\n\t\t\t;;\n\t\t\\?) echo \"Invalid option: -$OPTARG\" >&2\n\t\t\texit 1\n\t\t\t;;\n\t\t:)  echo \"Option -$OPTARG requires an argument.\" >&2\n\t\t\texit 1\n\t\t\t;;\n\tesac\ndone\nif [ -z \"$JPEGCNAME\" ]\nthen\n\tJPEGCNAME=\"imagemagick\"\nelif [ \"$JPEGCNAME\" != \"cjpeg\" -a \"$JPEGCNAME\" != \"jpegoptim\" -a \"$JPEGCNAME\" != \"jpge\" -a \"$JPEGCNAME\" != \"mozcjpeg\" ]\nthen\n\tJPEGCNAME=\"imagemagick\"\nfi\nOUTPUTFILESUFFIX=\"${OUTPUTFILESUFFIXBEGIN}_${JPEGCNAME}\"\necho \"$0 options: $DEFAULTCOMPRESSIONRATE, $HIGHCOMPRESSIONRATE, $TILESIZE, $OUTPUTFILESUFFIX\"\nshift \"$(($OPTIND - 1))\"\n\n###############################################################################\n# RUNTIME VARIABLES (usually do not require tuning by user)\n###############################################################################\n\n# Accept the jpg filename as a parameter\nFILE=\"$1\"\nif [ -z \"$FILE\" ]\nthen\n\techo \"${HELP}\"\n\texit 0\nfi\n\n# Retrieve clean filename without extension\nCLEANFILENAME=${FILE%.jp*g}\n\n# Retrieve only the file extension\nFILEEXTENSION=${FILE##*.}\n\n# Retrieve clean path directory without filename\nCLEANPATH=\"${FILE%/*}\"\n# If the JPEG is in the same direcctory as Adept, empty the path variable\n# Or if it is set, make sure the path has a trailing slash\nif [ \"$CLEANPATH\" == \"$FILE\" ]; then\n\tCLEANPATH=\"\"\nelse\n\tCLEANPATH=\"$CLEANPATH/\"\nfi\n\n# Storage location for all temporary files during runtime\n# Use locations like /dev/shm (/run/shm/ in Ubuntu) to save files in Shared Memory Space (RAM) to avoid disk i/o troubles\nTILESTORAGEPATH=\"/dev/shm/\"\n# Check if the directory for temporary image storage during runtime actually exists (honoring symlinks)\n# In case it does not, fall back to using \"/tmp/\" because it is very likely available on all Unix systems\nif [ ! -d \"$TILESTORAGEPATH\" ]; then\n\tTILESTORAGEPATH=\"/tmp/\"\nfi\n\n# Set locales to C (raw uninterpreted byte sequence)\n# to avoid Illegal byte sequence errors and invalid number errors\nexport LANG=C LC_NUMERIC=C LC_COLLATE=C\n\n\n\n###############################################################################\n# MAIN PROGRAM\n###############################################################################\n\nprepwork () {\n\tfind_tool IDENTIFY_COMMAND identify\n\tfind_tool CONVERT_COMMAND convert\n\tfind_tool MONTAGE_COMMAND montage\n\tif [ \"${JPEGCNAME}\" = \"cjpeg\" ]\n\tthen\n\t\tfind_tool JPEGCOMPRESSION_COMMAND cjpeg\n\t\tfind_tool JPEGDECOMPRESSION_COMMAND djpeg\n\telif [ \"${JPEGCNAME}\" = \"imagemagick\" ]\n\tthen\n\t\tfind_tool MOGRIFY_COMMAND mogrify\n\telif [ \"${JPEGCNAME}\" = \"jpegoptim\" ]\n\tthen\n\t\tfind_tool JPEGOPTIM_COMMAND jpegoptim\n\telif [ \"${JPEGCNAME}\" = \"jpge\" ]\n\tthen\n\t\tfind_tool JPEGCOMPRESSION_COMMAND jpge\n\telif [ \"${JPEGCNAME}\" = \"mozcjpeg\" ]\n\tthen\n\t\tfind_tool JPEGCOMPRESSION_COMMAND mozcjpeg\n\tfi\n\tfind_tool JPEGRESCAN_COMMAND jpegrescan\n\tfind_tool SALIENCYDETECTOR_COMMAND SaliencyDetector\n\tvalidate_image VALIDJPEG \"${FILE}\"\n}\n\nmain () {\n\tFILESIZE=\"$(stat -c %s ${FILE})\"\n\techo \"${FILESIZE} ${FILE}\"\n\tfind_image_dimension IMAGEWIDTH \"${FILE}\" 'w'\n\tfind_image_dimension IMAGEHEIGHT \"${FILE}\" 'h'\n\toptimize_tile_size TILESIZE ${TILESIZE} ${IMAGEWIDTH} ${IMAGEHEIGHT}\n\techo \"size=${TILESIZE} ${IMAGEWIDTH} ${IMAGEHEIGHT}\"\n\tcalculate_tile_count TILEROWS ${IMAGEHEIGHT} ${TILESIZE}\n\tcalculate_tile_count TILECOLUMNS ${IMAGEWIDTH} ${TILESIZE}\n\techo \"tilecount=${TILEROWS}x${TILECOLUMNS}\"\n\toptimize_salient_regions_amount BLACKWHITETHRESHOLD \"${FILE}\"\n\techo \"threshold=${BLACKWHITETHRESHOLD}\"\n\t${SALIENCYDETECTOR_COMMAND} -q -L0 -U${BLACKWHITETHRESHOLD} \"${FILE}\" \"${TILESTORAGEPATH}${CLEANFILENAME##*/}_saliency_bw.png\"\n\tslice_image_to_ram \"${FILE}\" ${TILESIZE} ${TILESTORAGEPATH}\n\testimate_content_complexity_and_compress\n\treassemble_tiles_into_final_image \"${FILESIZE}\"\n}\n\n\n\n###############################################################################\n# FUNCTIONS\n###############################################################################\n\nfloatToInt() {\n    printf \"%.0f\\n\" \"$@\"\n}\n\n# Find the proper handle for the required commandline tool\n# This function can take an optional third parameter when being called to manually define the path to the CLI tool\nfunction find_tool () {\n\t# Define local variables to work with\n\tlocal __result=$1\n\tlocal __tool=$2\n\tlocal __customtoolpath=$3\n\t# Array of possible tool locations: name, name as ALL-CAPS, /usr/bin/name, /usr/local/bin/name and custom path\n\tlocal __possibletoollocations=(${__tool} /usr/bin/${__tool} /usr/local/bin/${__tool} ${__customtoolpath})\n\t# For each possible tool location, test if its actually available there\n\tfor i in \"${__possibletoollocations[@]}\"; do\n\t\tlocal __commandlinetool=$(type -p $i)\n\t\t# If 'type -p' returned something, we now have our proper handle\n\t\tif [ \"$__commandlinetool\" ]; then\n\t\t\tbreak\n\t\tfi\n\tdone\n\t# In case none of the given inputs works, apologize & quit\n\tif [ ! \"$__commandlinetool\" ]; then\n\t\techo \"Unable to find ${__tool}. Please ensure that it is installed. If necessary, set its CLI path+name in the find_tool function call and then retry.\"\n\t\texit 1\n\tfi\n\t# Return the result\n\teval $__result=\"'${__commandlinetool}'\"\n}\n\n# Validate that we are working on an actual intact JPEG image before launch\nfunction validate_image () {\n\t# Define local variables to work with\n\tlocal __result=$1\n\tlocal __imagetovalidate=$2\n\t# If the script is called without an input file, explain how to use it\n\t# We don't \"exit 1\" here anymore because our unit tests source the script\n\t# and would abort if \"exit 1\" was called\n\tif [ ! -f \"$__imagetovalidate\" ]; then\n\t\tlocal __validationresult=0\n\t\techo \"Missing input JPEG. Usage: $0 /path/to/jpeg/image.jpg\"\n\telse\n\t\t# Use IM identify to read the file magic of the input file to validate it's a JPEG\n\t\tlocal __filemagic=$(${IDENTIFY_COMMAND} -format %m \"$__imagetovalidate\")\n\t\tif [ \"$__filemagic\" == \"JPEG\" ] ; then\n\t\t\t# Set a switch that it is ok to work on the input file, launching the main funtion\n\t\t\tlocal __validationresult=1\n\t\tfi\n\tfi\n\t# Return the result\n\teval $__result=\"'${__validationresult}'\"\n}\n\n# Read width (%w) or height (%h) of the input image via IM identify\nfunction find_image_dimension () {\n\t# Define local variables to work with\n\tlocal __result=$1\n\tlocal __imagetomeasure=$2\n\tlocal __dimensiontomeasure=$3\n\t# Read the width or height of the input image into a global variable\n\tlocal __imagedimension=$(${IDENTIFY_COMMAND} -format '%'${__dimensiontomeasure} ${__imagetomeasure})\n\t# Return the result\n\teval $__result=\"'${__imagedimension}'\"\n}\n\n# Tile size is the no.1 performance bottleneck for Adept, so it is important we pick an optimal tile size for the input image dimensions\n# Also, the number of tiles to be recombined affects compression efficiency and salient areas within an image tend to have similar dimensional\n# relations to total image size, so it makes sense to change tile size accordingly\nfunction optimize_tile_size () {\n\t# Define local variables to work with\n\tlocal __result=$1\n\tlocal __optimaltilesize=$2\n\tlocal __currentimagewidth=$3\n\tlocal __currentimageheight=$4\n\t# The default \"autodetect\" setting causes Adept to find a suitable tile size according to image dimensions\n\tif [ \"$TILESIZE\" == \"autodetect\" ] ; then\n\t\t# Pick the smaller of the two dimensions of the image as the decisive integer for tile size\n\t\tlocal __decisivedimension=${__currentimageheight}\n\t\tif (( $IMAGEWIDTH < $__decisivedimension )); then\n\t\t\t__decisivedimension=${__currentimagewidth}\n\t\tfi\n\t\t# For a series of sensible steps, change the tile size accordingly\n\t\tif (( $__decisivedimension <= 512 )); then\n\t\t\t__optimaltilesize=\"8\"\n\t\telif (( $__decisivedimension >= 513 )) && (( $__decisivedimension <= 1024 )); then\n\t\t\t__optimaltilesize=\"16\"\n\t\telif (( $__decisivedimension >= 1025 )); then\n\t\t\t__optimaltilesize=\"32\"\n\t\telse\n\t\t\t__optimaltilesize=\"8\"\n\t\tfi\n\t# In case the user has changed the configuration from \"autodetect\" to a custom setting, respect & return this instead\n\telse\n\t\t__optimaltilesize=${TILESIZE}\n\tfi\n\t# Return the result\n\teval $__result=\"'${__optimaltilesize}'\"\n}\n\nfunction optimize_salient_regions_amount () {\n\t# Define local variables to work with\n\tlocal __result=$1\n\tlocal __imagetomeasure=$2\n\tlocal __lower_bound=\"0\"\n\tlocal __upper_bound=\"100\"\n\tlocal __current_threshold=$(( $__upper_bound/2 ))\n\tlocal __mean_graychannel=\"0\"\n\t# Run the saliency detector function to retrieve the Median gray channel\n\tcalculate_salient_regions_amount __mean_graychannel \"${__imagetomeasure}\" ${__upper_bound}\n\n\t__mean_graychannel=$(floatToInt $__mean_graychannel)\n\n\t# If we didn't hit the sweet spot on our initial run, keep homing in on the ideal threshold value using binary search\n\twhile ( (( $__mean_graychannel > 40 )) || (( $__mean_graychannel < 20  )) ) && (( $__lower_bound < $__upper_bound-1 )); do\n\t\t# If the Median is too low, reduce the upper threshold value to get more white pixels\n\t\tif (( $__mean_graychannel < 20 )); then\n\t\t\t__upper_bound=${__current_threshold}\n\t\t# Else if the Median is too high, raise the threshold to get fewer white pixels\n\t\telif (( $__mean_graychannel > 40 )); then\n\t\t\t__lower_bound=${__current_threshold}\n\t\tfi\n\t\t# Calculate the new middle threshold\n\t\t__current_threshold=$(( ($__upper_bound-$__lower_bound)/2+$__lower_bound ))\n\t\t# Rerun the saliency detector with a better estimated threshold value\n\t\tcalculate_salient_regions_amount __mean_graychannel \"${__imagetomeasure}\" ${__current_threshold}\n\n\t\t__mean_graychannel=$(floatToInt $__mean_graychannel)\n\tdone\n\t# Return result\n\teval $__result=\"'${__current_threshold}'\"\n}\n\n# Measure the black/white median of a saliency mapped image to use it as an indicator for successfull saliency mapped contents\nfunction calculate_salient_regions_amount () {\n\t# Define local variables to work with\n\tlocal __result=$1\n\tlocal __imagetomeasure=$2\n\tlocal __threshold=$3\n\t# Use the MSS Saliency Detector with custom thresholds to generate a black+white salient map of an input image\n\t# Then use the gray channel's mean as a single indicator to judge how much of the image's contents have been marked as salient\n\tlocal __salient_amount=$(${SALIENCYDETECTOR_COMMAND} -q -L0 -U${__threshold} \"${__imagetomeasure}\" \"png:-\" | ${IDENTIFY_COMMAND} -channel Gray -format \"%[fx:255*mean]\" -)\n\techo \"salient=${__salient_amount}\"\n\t# Return result\n\teval $__result=\"'${__salient_amount}'\"\n}\n\n# Slice the input image into equally sized tiles\nfunction slice_image_to_ram () {\n\t# Define local variables to work with\n\tlocal __filetoprocess=$1\n\tlocal __currenttilesize=$2\n\tlocal __currenttilestoragepath=$3\n\techo -n \"slice to ram...\"\n\t# If $DEFAULTCOMPRESSIONRATE is set to \"inherit\", discover the input JPG quality\n\tif [ \"$DEFAULTCOMPRESSIONRATE\" == \"inherit\" ] ; then\n\t\tDEFAULTCOMPRESSIONRATE=$(${IDENTIFY_COMMAND} -format \"%Q\" ${__filetoprocess})\n\tfi\n\techo \" ok.\"\n\t${CONVERT_COMMAND} \"$__filetoprocess\" -strip -quality \"${DEFAULTCOMPRESSIONRATE}\" -define jpeg:dct-method=default -crop \"${__currenttilesize}\"x\"${__currenttilesize}\" +repage +adjoin \"${__currenttilestoragepath}tile_tmp_%06d_${CLEANFILENAME##*/}.${FILEEXTENSION}\"\n}\n\n# For each tile, test if it is suitable for higher compression and if so, proceed\nfunction estimate_content_complexity_and_compress () {\n\t# Set up a counter so we keep track of the full name of the current temporary tile to work on\n\tlocal __currenttilecount=0\n\t# Let's create a walker that iterates over the sobeled and b/w reduced full size image\n\t# This way, the edge detection happens only in memory and does not need additional tiles to be created on the filesystem\n\t# The walker inputs X+Y coordinates and only analyses a single tile's size on that spot within the image\n\t# The exception to this being when we are close to the image's end and we have to reduce tile size to whatever is left vertically or horizontally\n\techo -n \"estimate content complexity and compress...\"\n\tfor((y=0;y<$TILEROWS;y++)) ; do\n\t\tfor((x=0;x<$TILECOLUMNS;x++)) ; do\n\t\t\t# Reset tile dimensions for each run because we need to check them anew each time\n\t\t\tlocal __currenttileheight=${TILESIZE}\n\t\t\tlocal __currenttilewidth=${TILESIZE}\n\t\t\t# Prepend leading zeros to the counter so the integer matches the numbers handed out to the filename by ImageMagick\n\t\t\t__currenttilecount=$(printf \"%06d\" $__currenttilecount);\n\t\t\t# If we are nearing the end of the image height, reduce tile size to whatever is left vertically\n\t\t\tif (( $y + 1 == $TILEROWS )) && (( $TILEROWS * $__currenttileheight > $IMAGEHEIGHT )); then\n\t\t\t\t__currenttileheight=$(( (($y+1)*$TILESIZE) - $IMAGEHEIGHT ))\n\t\t\t\t__currenttilerow=$(( $y+1 ))\n\t\t\tfi\n\t\t\t# And if we are nearing the end of the image width, reduce tile size to whatever is left horizontally\n\t\t\tif (( $x + 1 == $TILECOLUMNS )) && (( $TILECOLUMNS * $__currenttilewidth > $IMAGEWIDTH )); then\n\t\t\t\t__currenttilewidth=$(( (($x+1)*$TILESIZE) - $IMAGEWIDTH ))\n\t\t\t\t__currenttilecolumn=$(( $x+1 ))\n\t\t\tfi\n\t\t\t# Run identify on the 2-color limited palette PNG8 to retrieve the mean for the gray channel\n\t\t\t# In this case we are using coordinates and dynamic tile sizes according to the walker logic we have created in order to dynamically view a specific image area without creating actual tiles for it\n\t\t\t# The result will be a decimal number (or zero) by which we can judge the visible object complexity in the current tile\n\t\t\tlocal __currentbwmedian=$(identify -size \"${IMAGEWIDTH}\"x\"${IMAGEHEIGHT}\" -channel Gray -format \"%[fx:255*mean]\" \"${TILESTORAGEPATH}${CLEANFILENAME##*/}_saliency_bw.png[\"${__currenttilewidth}\"x\"${__currenttileheight}\"+$(echo $((${x}*${__currenttilewidth})))+$(echo $((${y}*${__currenttileheight})))]\")\n\t\t\t# If the gray channel median is below a defined threshold, the visible area in the current tile is very likely simple & rather monotonous and can safely be exposed to a higher compression rate\n\t\t\t# Untouched JPGs simply stay at the defined default quality setting ($DEFAULTCOMPRESSIONRATE)\n\t\t\tif (( $(echo \"$__currentbwmedian < 0.825\" | bc) )); then\n\t\t\t\t# We experimented with blurring/smoothing of tiles here to enhance JPEG compression, but results were insignificant\n\t\t\t\tif [ \"${JPEGCNAME}\" = \"cjpeg\" ]\n\t\t\t\tthen\n\t\t\t\t\t${JPEGDECOMPRESSION_COMMAND} \"${TILESTORAGEPATH}\"tile_tmp_\"${__currenttilecount}\"_\"${CLEANFILENAME##*/}\".\"${FILEEXTENSION}\" | ${JPEGCOMPRESSION_COMMAND} -progressive -optimize -quality ${HIGHCOMPRESSIONRATE} -outfile \"${TILESTORAGEPATH}\"tile_tmp_\"${__currenttilecount}\"_\"${CLEANFILENAME##*/}\"_cjpeg.\"${FILEEXTENSION}\"\n\t\t\t\t\tmv \"${TILESTORAGEPATH}\"tile_tmp_\"${__currenttilecount}\"_\"${CLEANFILENAME##*/}\"_cjpeg.\"${FILEEXTENSION}\" \"${TILESTORAGEPATH}\"tile_tmp_\"${__currenttilecount}\"_\"${CLEANFILENAME##*/}\".\"${FILEEXTENSION}\"\n\t\t\t\telif [ \"${JPEGCNAME}\" = \"imagemagick\" ]\n\t\t\t\tthen\n\t\t\t\t\t${MOGRIFY_COMMAND}  -quality ${HIGHCOMPRESSIONRATE} \"${TILESTORAGEPATH}\"tile_tmp_\"${__currenttilecount}\"_\"${CLEANFILENAME##*/}\".\"${FILEEXTENSION}\"\n\t\t\t\telif [ \"${JPEGCNAME}\" = \"jpegoptim\" ]\n\t\t\t\tthen\n\t\t\t\t\t${JPEGOPTIM_COMMAND} --max=${HIGHCOMPRESSIONRATE} --strip-all --strip-iptc --strip-icc \"${TILESTORAGEPATH}\"tile_tmp_\"${__currenttilecount}\"_\"${CLEANFILENAME##*/}\".\"${FILEEXTENSION}\" >/dev/null 2>/dev/null\n\t\t\t\telif [ \"${JPEGCNAME}\" = \"jpge\" ]\n\t\t\t\tthen\n\t\t\t\t\t${JPEGCOMPRESSION_COMMAND}  \"${TILESTORAGEPATH}\"tile_tmp_\"${__currenttilecount}\"_\"${CLEANFILENAME##*/}\".\"${FILEEXTENSION}\" \"${TILESTORAGEPATH}\"tile_tmp_\"${__currenttilecount}\"_\"${CLEANFILENAME##*/}\"_jpge.\"${FILEEXTENSION}\" ${HIGHCOMPRESSIONRATE} > /dev/null\n\t\t\t\t\tmv \"${TILESTORAGEPATH}\"tile_tmp_\"${__currenttilecount}\"_\"${CLEANFILENAME##*/}\"_jpge.\"${FILEEXTENSION}\" \"${TILESTORAGEPATH}\"tile_tmp_\"${__currenttilecount}\"_\"${CLEANFILENAME##*/}\".\"${FILEEXTENSION}\"\n\t\t\t\telif [ \"${JPEGCNAME}\" = \"mozcjpeg\" ]\n\t\t\t\tthen\n\t\t\t\t\t${JPEGCOMPRESSION_COMMAND} -progressive -optimize -quality ${HIGHCOMPRESSIONRATE} -outfile \"${TILESTORAGEPATH}\"tile_tmp_\"${__currenttilecount}\"_\"${CLEANFILENAME##*/}\"_mozjpeg.\"${FILEEXTENSION}\" \"${TILESTORAGEPATH}\"tile_tmp_\"${__currenttilecount}\"_\"${CLEANFILENAME##*/}\".\"${FILEEXTENSION}\"\n\t\t\t\t\tmv \"${TILESTORAGEPATH}\"tile_tmp_\"${__currenttilecount}\"_\"${CLEANFILENAME##*/}\"_mozjpeg.\"${FILEEXTENSION}\" \"${TILESTORAGEPATH}\"tile_tmp_\"${__currenttilecount}\"_\"${CLEANFILENAME##*/}\".\"${FILEEXTENSION}\"\n\t\t\t\tfi\n\t\t\tfi\n\t\t\t# As the last action within the loop, increment the counter for the processed tile number. Use Base10 because with the padding of leading zeros, Bash would interprete the integer as Base8 per default.\n\t\t\t__currenttilecount=$(( 10#$__currenttilecount + 1 ))\n\t\tdone\n\tdone\n\techo \" ${__currenttilecount} tile ok.\"\n}\n\n# For the reassembly of the image, we need the count of rows and columns of tiles that were created\nfunction calculate_tile_count () {\n\t# Define local variables to work with\n\tlocal __result=$1\n\tlocal __currentimagedimension=$2\n\tlocal __currenttilesize=$3\n\t# Make use of Bash's behaviour of rounding down to see if we're tilecount = integer + 1\n\tlocal __tilecountroundeddown=$(( $__currentimagedimension / $__currenttilesize ))\n\t# Check if we need to +1 our integer because the decimal is larger than the integer\n\tif (( $__currenttilesize * $__tilecountroundeddown < $__currentimagedimension )); then\n\t\tlocal __tilecount=$(( $__tilecountroundeddown + 1 ))\n\telse\n\t\tlocal __tilecount=${__tilecountroundeddown}\n\tfi\n\t# Return result\n\teval $__result=\"'${__tilecount}'\"\n}\n\n# Now that we know the number of rows+columns, we use montage to recombine the now partially compressed tiles into a new coherant JPEG image\nfunction reassemble_tiles_into_final_image () {\n\tlocal __filesize=\"$1\"\n\techo -n \"final image...\"\n\t# Use montage to reassemble the individual, partially optimized tiles into a new consistent JPEG image\n\tFILEOUT=\"${CLEANPATH}${CLEANFILENAME##*/}${OUTPUTFILESUFFIX}.${FILEEXTENSION}\"\n\t${MONTAGE_COMMAND} -quiet -strip -quality \"${DEFAULTCOMPRESSIONRATE}\" -mode concatenate -tile \"${TILECOLUMNS}x${TILEROWS}\" $(find \"${TILESTORAGEPATH}\" -maxdepth 1 -type f -name \"tile_tmp_*_${CLEANFILENAME##*/}.${FILEEXTENSION}\" | sort) \"$FILEOUT\" >/dev/null 2>/dev/null\n\n\t# During montage reassembly, the resulting image received bytes of padding due to the way the JPEG compression algorithm works on tiles not sized as a multiple of 8\n\t# So we run jpegrescan on the final image to losslessly remove this padding and make the output JPG progressive\n\t${JPEGRESCAN_COMMAND} -q -s -i \"${FILEOUT}\" \"${FILEOUT}\"\n\n\t# Cleanup temporary files\n\trm ${TILESTORAGEPATH}${CLEANFILENAME##*/}_saliency_bw.png\n\t# We are using find to circumvent issues on Kernel based shell limitations when iterating over a large number of files with rm\n\tfind \"${TILESTORAGEPATH}\" -maxdepth 1 -type f -name \"tile_tmp_*_${CLEANFILENAME##*/}.${FILEEXTENSION}\" -exec rm {} \\;\n\techo \" ok.\"\n\tFILEOUTSIZE=\"$(stat -c %s ${FILEOUT})\"\n\tlet \"PERCENT=(${FILEOUTSIZE} * 100 + ${__filesize} / 2)/ ${__filesize}\"\n\techo \"${FILEOUTSIZE} ${FILEOUT} (${PERCENT}%)\"\n}\n\n# Initiate preparatory checks\nprepwork\n# If the preparations worked, launch the main program\nif (( VALIDJPEG )); then\n\tmain\nfi\n\n\n\n###############################################################################\n# EOF\n###############################################################################\n"
  },
  {
    "path": "adept.sh",
    "content": "#!/usr/bin/env bash\n\n###############################################################################\n#\n# Bash script to automate adaptive JPEG compression using common CLI tools\n#\n# Usage: bash adept.sh /path/to/image.jpg\n#\n###############################################################################\n#\n# Brief overview of the mode of operation:\n#\n# The input JPG gets sliced into tiles, sized as a multiple of 8 due to the\n# nature of JPG compression. The image is also run through a saliency\n# detection algorithm and its resulting output further reduced to a\n# 2-color black+white PNG.\n#\n# This bi-color PNG is ideal to measure tiles' gray channel mean value and use\n# it as a single integer indicator to judge its perceivable complexity.\n#\n# Areas with low complexity contents are then exposed to heavier compression.\n# At reassemlby, this leads to savings in image bytesize while maintaining\n# good visual quality because no compression artefacts occur in areas of\n# high-complexity or sharp contrasts.\n#\n###############################################################################\n# Tools that need to be pre-installed:\n#\n#\t* Maximum Symmetric Surround Saliency Algorithm Binary\n#\t http://github.com/technopagan/mss-saliency\n#\n#\t* ImageMagick >= v.6.6\n#\n#\t* MozJPEG\n#\t http://github.com/mozilla/mozjpeg\n#\t Expects Mozjpeg to be available under 'mozjpeg', e.g. via symlink\n#\n#\t* JPEGRescan Perl Script for lossless JPG compression\n#\t http://github.com/kud/jpegrescan\n#\n# Note: Additonal tools are required to run Adept, such as \"bc\",\n# \"find\", \"mv\", \"rm\" and Bash 3.x. As all of these tools are provided by lsbcore, core-utils\n# or similar default packages, we can expect them to be always available.\n#\n###############################################################################\n#\n# This software is published under the BSD licence 3.0\n#\n# Copyright (c) 2013-2015, Tobias Baldauf\n# All rights reserved.\n#\n# Mail: kontakt@tobias-baldauf.de\n# Web: http://who.tobias.is/\n# Twitter: @tbaldauf\n#\n# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n#\n#\t* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n#\n#\t* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\n#\n#\t* Neither the name of the author nor the names of contributors may be used to endorse or promote products derived from this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n#\n###############################################################################\n\n\n\n###############################################################################\n# USER CONFIGURABLE PARAMETERS\n###############################################################################\n\n# Default JPG quality setting, either inherited or defined as an integer of 0-100\n# Default: inherit\nDEFAULTCOMPRESSIONRATE=\"inherit\"\n\n# JPEG quality setting for areas of the image deemed suitable for high compression in an integer of 0-100\n# Default: 69\nHIGHCOMPRESSIONRATE=\"69\"\n\n# Suffix string to attach to the output JPG filename, e.g. '_adept_compress'\n# If deliberatly set empty (''), the input JPG will be replaced with the new compressed JPG\nOUTPUTFILESUFFIX=\"_adept_compress\"\n\n\n###############################################################################\n# RUNTIME VARIABLES (usually do not require tuning by user)\n###############################################################################\n\n# Accept the jpg filename as a parameter\nFILE=\"$1\"\n\n# Retrieve clean filename without extension\nCLEANFILENAME=${FILE%.jp*g}\n\n# Retrieve only the file extension\nFILEEXTENSION=${FILE##*.}\n\n# Retrieve clean path directory without filename\nCLEANPATH=\"${FILE%/*}\"\n# If the JPEG is in the same direcctory as Adept, empty the path variable\n# Or if it is set, make sure the path has a trailing slash\nif [ \"$CLEANPATH\" == \"$FILE\" ]; then\n\tCLEANPATH=\"\"\nelse\n\tCLEANPATH=\"$CLEANPATH/\"\nfi\n\n# Storage location for all temporary files during runtime\n# Use locations like /dev/shm (/run/shm/ in Ubuntu) to save files in Shared Memory Space (RAM) to avoid disk i/o troubles\nTILESTORAGEPATH=\"/dev/shm/\"\n# Check if the directory for temporary image storage during runtime actually exists (honoring symlinks)\n# In case it does not, fall back to using \"/tmp/\" because it is very likely available on all Unix systems\nif [ ! -d \"$TILESTORAGEPATH\" ]; then\n\tTILESTORAGEPATH=\"/tmp/\"\nfi\n\n# Square dimensions for all temporary tiles. Tile size heavily influences compression efficiency at the cost of runtime performance\n# E.g. a tile size of 8 yields maximum compression results while taking several minutes of runtime\n# If you chose to manually adjust tile size, only use multiples of 8 (8/16/32/64/128/256)\n# Default: autodetect\nTILESIZE=\"autodetect\"\n\n# Set locales to C (raw uninterpreted byte sequence)\n# to avoid Illegal byte sequence errors and invalid number errors\nexport LANG=C LC_NUMERIC=C LC_COLLATE=C\n\n\n###############################################################################\n# COMMAND LINE OPTIONS\n###############################################################################\n\n# Allow user to specify -c, -h or -o on the command line for the compression rate, high compression rate, and file suffix\n\nusage() {\n\techo \"Usage: $0 [options] /path/to/jpeg/image.jpg\nOptions (and defaults):\n\t-c INT    Default compression rate ($DEFAULTCOMPRESSIONRATE)\n\t-h INT    High compression rate ($HIGHCOMPRESSIONRATE)\n\t-o SUFF   Output suffix ($OUTPUTFILESUFFIX)\n\"\n\texit 1\n}\n\nwhile getopts \"c:h:o:\" optionName; do\ncase \"$optionName\" in\n        c) DEFAULTCOMPRESSIONRATE=\"$OPTARG\";;\n        h) HIGHCOMPRESSIONRATE=\"$OPTARG\";;\n        o) OUTPUTFILESUFFIX=\"$OPTARG\";;\n        \\?) usage;;\nesac\ndone\nshift `expr $OPTIND - 1`\n\n[ -z \"$1\" ] && usage\n\n###############################################################################\n# MAIN PROGRAM\n###############################################################################\n\nprepwork () {\n\tfind_tool IDENTIFY_COMMAND identify\n\tfind_tool CONVERT_COMMAND convert\n\tfind_tool MONTAGE_COMMAND montage\n\tfind_tool JPEGCOMPRESSION_COMMAND mozjpeg\n\tfind_tool JPEGRESCAN_COMMAND jpegrescan\n\tfind_tool SALIENCYDETECTOR_COMMAND SaliencyDetector\n\tvalidate_image VALIDJPEG \"${FILE}\"\n}\n\nmain () {\n\tfind_image_dimension IMAGEWIDTH \"${FILE}\" 'w'\n\tfind_image_dimension IMAGEHEIGHT \"${FILE}\" 'h'\n\toptimize_tile_size TILESIZE ${TILESIZE} ${IMAGEWIDTH} ${IMAGEHEIGHT}\n\tcalculate_tile_count TILEROWS ${IMAGEHEIGHT} ${TILESIZE}\n\tcalculate_tile_count TILECOLUMNS ${IMAGEWIDTH} ${TILESIZE}\n\toptimize_salient_regions_amount BLACKWHITETHRESHOLD \"${FILE}\"\n\t${SALIENCYDETECTOR_COMMAND} -q -L0 -U${BLACKWHITETHRESHOLD} \"${FILE}\" \"${TILESTORAGEPATH}${CLEANFILENAME##*/}_saliency_bw.png\"\n\tslice_image_to_ram \"${FILE}\" ${TILESIZE} ${TILESTORAGEPATH}\n\testimate_content_complexity_and_compress\n\treassemble_tiles_into_final_image\n}\n\n\n\n###############################################################################\n# FUNCTIONS\n###############################################################################\n\nfloatToInt() {\n    printf \"%.0f\\n\" \"$@\"\n}\n\n# Find the proper handle for the required commandline tool\n# This function can take an optional third parameter when being called to manually define the path to the CLI tool\nfunction find_tool () {\n\t# Define local variables to work with\n\tlocal __result=$1\n\tlocal __tool=$2\n\tlocal __customtoolpath=$3\n\t# Array of possible tool locations: name, name as ALL-CAPS, /usr/bin/name, /usr/local/bin/name and custom path\n\tlocal __possibletoollocations=(${__tool} /usr/bin/${__tool} /usr/local/bin/${__tool} ${__customtoolpath})\n\t# For each possible tool location, test if its actually available there\n\tfor i in \"${__possibletoollocations[@]}\"; do\n\t\tlocal __commandlinetool=$(type -p $i)\n\t\t# If 'type -p' returned something, we now have our proper handle\n\t\tif [ \"$__commandlinetool\" ]; then\n\t\t\tbreak\n\t\tfi\n\tdone\n\t# In case none of the given inputs works, apologize & quit\n\tif [ ! \"$__commandlinetool\" ]; then\n\t\techo \"Unable to find ${__tool}. Please ensure that it is installed. If necessary, set its CLI path+name in the find_tool function call and then retry.\"\n\t\texit 1\n\tfi\n\t# Return the result\n\teval $__result=\"'${__commandlinetool}'\"\n}\n\n# Validate that we are working on an actual intact JPEG image before launch\nfunction validate_image () {\n\t# Define local variables to work with\n\tlocal __result=$1\n\tlocal __imagetovalidate=$2\n\t# If the script is called without an input file, explain how to use it\n\t# We don't \"exit 1\" here anymore because our unit tests source the script\n\t# and would abort if \"exit 1\" was called\n\tif [ ! -f \"$__imagetovalidate\" ]; then\n\t\tlocal __validationresult=0\n\t\techo \"Missing input JPEG. Usage: $0 /path/to/jpeg/image.jpg\"\n\telse\n\t\t# Use IM identify to read the file magic of the input file to validate it's a JPEG\n\t\tlocal __filemagic=$(${IDENTIFY_COMMAND} -format %m \"$__imagetovalidate\")\n\t\tif [ \"$__filemagic\" == \"JPEG\" ] ; then\n\t\t\t# Set a switch that it is ok to work on the input file, launching the main funtion\n\t\t\tlocal __validationresult=1\n\t\tfi\n\tfi\n\t# Return the result\n\teval $__result=\"'${__validationresult}'\"\n}\n\n# Read width (%w) or height (%h) of the input image via IM identify\nfunction find_image_dimension () {\n\t# Define local variables to work with\n\tlocal __result=$1\n\tlocal __imagetomeasure=$2\n\tlocal __dimensiontomeasure=$3\n\t# Read the width or height of the input image into a global variable\n\tlocal __imagedimension=$(${IDENTIFY_COMMAND} -format '%'${__dimensiontomeasure} ${__imagetomeasure})\n\t# Return the result\n\teval $__result=\"'${__imagedimension}'\"\n}\n\n# Tile size is the no.1 performance bottleneck for Adept, so it is important we pick an optimal tile size for the input image dimensions\n# Also, the number of tiles to be recombined affects compression efficiency and salient areas within an image tend to have similar dimensional\n# relations to total image size, so it makes sense to change tile size accordingly\nfunction optimize_tile_size () {\n\t# Define local variables to work with\n\tlocal __result=$1\n\tlocal __optimaltilesize=$2\n\tlocal __currentimagewidth=$3\n\tlocal __currentimageheight=$4\n\t# The default \"autodetect\" setting causes Adept to find a suitable tile size according to image dimensions\n\tif [ \"$TILESIZE\" == \"autodetect\" ] ; then\n\t\t# Pick the smaller of the two dimensions of the image as the decisive integer for tile size\n\t\tlocal __decisivedimension=${__currentimageheight}\n\t\tif (( $IMAGEWIDTH < $__decisivedimension )); then\n\t\t\t__decisivedimension=${__currentimagewidth}\n\t\tfi\n\t\t# For a series of sensible steps, change the tile size accordingly\n\t\tif (( $__decisivedimension <= 512 )); then\n\t\t\t__optimaltilesize=\"8\"\n\t\telif (( $__decisivedimension >= 513 )) && (( $__decisivedimension <= 1024 )); then\n\t\t\t__optimaltilesize=\"16\"\n\t\telif (( $__decisivedimension >= 1025 )); then\n\t\t\t__optimaltilesize=\"32\"\n\t\telse\n\t\t\t__optimaltilesize=\"8\"\n\t\tfi\n\t# In case the user has changed the configuration from \"autodetect\" to a custom setting, respect & return this instead\n\telse\n\t\t__optimaltilesize=${TILESIZE}\n\tfi\n\t# Return the result\n\teval $__result=\"'${__optimaltilesize}'\"\n}\n\nfunction optimize_salient_regions_amount () {\n\t# Define local variables to work with\n\tlocal __result=$1\n\tlocal __imagetomeasure=$2\n\tlocal __lower_bound=\"0\"\n\tlocal __upper_bound=\"100\"\n\tlocal __current_threshold=$(( $__upper_bound/2 ))\n\tlocal __mean_graychannel=\"0\"\n\t# Run the saliency detector function to retrieve the Median gray channel\n\tcalculate_salient_regions_amount __mean_graychannel \"${__imagetomeasure}\" ${__upper_bound}\n\n\t__mean_graychannel=$(floatToInt $__mean_graychannel)\n\n\t# If we didn't hit the sweet spot on our initial run, keep homing in on the ideal threshold value using binary search\n\twhile ( (( $__mean_graychannel > 40 )) || (( $__mean_graychannel < 20  )) ) && (( $__lower_bound < $__upper_bound-1 )); do\n\t\t# If the Median is too low, reduce the upper threshold value to get more white pixels\n\t\tif (( $__mean_graychannel < 20 )); then\n\t\t\t__upper_bound=${__current_threshold}\n\t\t# Else if the Median is too high, raise the threshold to get fewer white pixels\n\t\telif (( $__mean_graychannel > 40 )); then\n\t\t\t__lower_bound=${__current_threshold}\n\t\tfi\n\t\t# Calculate the new middle threshold\n\t\t__current_threshold=$(( ($__upper_bound-$__lower_bound)/2+$__lower_bound ))\n\t\t# Rerun the saliency detector with a better estimated threshold value\n\t\tcalculate_salient_regions_amount __mean_graychannel \"${__imagetomeasure}\" ${__current_threshold}\n\n\t\t__mean_graychannel=$(floatToInt $__mean_graychannel)\n\tdone\n\t# Return result\n\teval $__result=\"'${__current_threshold}'\"\n}\n\n# Measure the black/white median of a saliency mapped image to use it as an indicator for successfull saliency mapped contents\nfunction calculate_salient_regions_amount () {\n\t# Define local variables to work with\n\tlocal __result=$1\n\tlocal __imagetomeasure=$2\n\tlocal __threshold=$3\n\t# Use the MSS Saliency Detector with custom thresholds to generate a black+white salient map of an input image\n\t# Then use the gray channel's mean as a single indicator to judge how much of the image's contents have been marked as salient\n\tlocal __salient_amount=$(${SALIENCYDETECTOR_COMMAND} -q -L0 -U${__threshold} \"${__imagetomeasure}\" \"png:-\" | ${IDENTIFY_COMMAND} -channel Gray -format \"%[fx:255*mean]\" -)\n\t# Return result\n\teval $__result=\"'${__salient_amount}'\"\n}\n\n# Slice the input image into equally sized tiles\nfunction slice_image_to_ram () {\n\t# Define local variables to work with\n\tlocal __filetoprocess=$1\n\tlocal __currenttilesize=$2\n\tlocal __currenttilestoragepath=$3\n\t# If $DEFAULTCOMPRESSIONRATE is set to \"inherit\", discover the input JPG quality\n\tif [ \"$DEFAULTCOMPRESSIONRATE\" == \"inherit\" ] ; then\n\t\tDEFAULTCOMPRESSIONRATE=$(${IDENTIFY_COMMAND} -format \"%Q\" ${__filetoprocess})\n\tfi\n\t${CONVERT_COMMAND} \"$__filetoprocess\" -strip -quality \"${DEFAULTCOMPRESSIONRATE}\" -define jpeg:dct-method=default -crop \"${__currenttilesize}\"x\"${__currenttilesize}\" +repage +adjoin \"${__currenttilestoragepath}tile_tmp_%06d_${CLEANFILENAME##*/}.${FILEEXTENSION}\"\n}\n\n# For each tile, test if it is suitable for higher compression and if so, proceed\nfunction estimate_content_complexity_and_compress () {\n\t# Set up a counter so we keep track of the full name of the current temporary tile to work on\n\tlocal __currenttilecount=0\n\t# Let's create a walker that iterates over the sobeled and b/w reduced full size image\n\t# This way, the edge detection happens only in memory and does not need additional tiles to be created on the filesystem\n\t# The walker inputs X+Y coordinates and only analyses a single tile's size on that spot within the image\n\t# The exception to this being when we are close to the image's end and we have to reduce tile size to whatever is left vertically or horizontally\n\tfor((y=0;y<$TILEROWS;y++)) ; do\n\t\tfor((x=0;x<$TILECOLUMNS;x++)) ; do\n\t\t\t# Reset tile dimensions for each run because we need to check them anew each time\n\t\t\tlocal __currenttileheight=${TILESIZE}\n\t\t\tlocal __currenttilewidth=${TILESIZE}\n\t\t\t# Prepend leading zeros to the counter so the integer matches the numbers handed out to the filename by ImageMagick\n\t\t\t__currenttilecount=$(printf \"%06d\" $__currenttilecount);\n\t\t\t# If we are nearing the end of the image height, reduce tile size to whatever is left vertically\n\t\t\tif (( $y + 1 == $TILEROWS )) && (( $TILEROWS * $__currenttileheight > $IMAGEHEIGHT )); then\n\t\t\t\t__currenttileheight=$(( (($y+1)*$TILESIZE) - $IMAGEHEIGHT ))\n\t\t\t\t__currenttilerow=$(( $y+1 ))\n\t\t\tfi\n\t\t\t# And if we are nearing the end of the image width, reduce tile size to whatever is left horizontally\n\t\t\tif (( $x + 1 == $TILECOLUMNS )) && (( $TILECOLUMNS * $__currenttilewidth > $IMAGEWIDTH )); then\n\t\t\t\t__currenttilewidth=$(( (($x+1)*$TILESIZE) - $IMAGEWIDTH ))\n\t\t\t\t__currenttilecolumn=$(( $x+1 ))\n\t\t\tfi\n\t\t\t# Run identify on the 2-color limited palette PNG8 to retrieve the mean for the gray channel\n\t\t\t# In this case we are using coordinates and dynamic tile sizes according to the walker logic we have created in order to dynamically view a specific image area without creating actual tiles for it\n\t\t\t# The result will be a decimal number (or zero) by which we can judge the visible object complexity in the current tile\n\t\t\tlocal __currentbwmedian=$(identify -size \"${IMAGEWIDTH}\"x\"${IMAGEHEIGHT}\" -channel Gray -format \"%[fx:255*mean]\" \"${TILESTORAGEPATH}${CLEANFILENAME##*/}_saliency_bw.png[\"${__currenttilewidth}\"x\"${__currenttileheight}\"+$(echo $((${x}*${__currenttilewidth})))+$(echo $((${y}*${__currenttileheight})))]\")\n\t\t\t# If the gray channel median is below a defined threshold, the visible area in the current tile is very likely simple & rather monotonous and can safely be exposed to a higher compression rate\n\t\t\t# Untouched JPGs simply stay at the defined default quality setting ($DEFAULTCOMPRESSIONRATE)\n\t\t\tif (( $(echo \"$__currentbwmedian < 0.825\" | bc) )); then\n\t\t\t\t# We experimented with blurring/smoothing of tiles here to enhance JPEG compression, but results were insignificant\n\t\t\t\t${JPEGCOMPRESSION_COMMAND} -progressive -optimize -quality ${HIGHCOMPRESSIONRATE} -outfile \"${TILESTORAGEPATH}\"tile_tmp_\"${__currenttilecount}\"_\"${CLEANFILENAME##*/}\"_mozjpeg.\"${FILEEXTENSION}\" \"${TILESTORAGEPATH}\"tile_tmp_\"${__currenttilecount}\"_\"${CLEANFILENAME##*/}\".\"${FILEEXTENSION}\"\n\t\t\t\tmv \"${TILESTORAGEPATH}\"tile_tmp_\"${__currenttilecount}\"_\"${CLEANFILENAME##*/}\"_mozjpeg.\"${FILEEXTENSION}\" \"${TILESTORAGEPATH}\"tile_tmp_\"${__currenttilecount}\"_\"${CLEANFILENAME##*/}\".\"${FILEEXTENSION}\"\n\t\t\tfi\n\t\t\t# As the last action within the loop, increment the counter for the processed tile number. Use Base10 because with the padding of leading zeros, Bash would interprete the integer as Base8 per default.\n\t\t\t__currenttilecount=$(( 10#$__currenttilecount + 1 ))\n\t\tdone\n\tdone\n}\n\n# For the reassembly of the image, we need the count of rows and columns of tiles that were created\nfunction calculate_tile_count () {\n\t# Define local variables to work with\n\tlocal __result=$1\n\tlocal __currentimagedimension=$2\n\tlocal __currenttilesize=$3\n\t# Make use of Bash's behaviour of rounding down to see if we're tilecount = integer + 1\n\tlocal __tilecountroundeddown=$(( $__currentimagedimension / $__currenttilesize ))\n\t# Check if we need to +1 our integer because the decimal is larger than the integer\n\tif (( $__currenttilesize * $__tilecountroundeddown < $__currentimagedimension )); then\n\t\tlocal __tilecount=$(( $__tilecountroundeddown + 1 ))\n\telse\n\t\tlocal __tilecount=${__tilecountroundeddown}\n\tfi\n\t# Return result\n\teval $__result=\"'${__tilecount}'\"\n}\n\n# Now that we know the number of rows+columns, we use montage to recombine the now partially compressed tiles into a new coherant JPEG image\nfunction reassemble_tiles_into_final_image () {\n\t# Use montage to reassemble the individual, partially optimized tiles into a new consistent JPEG image\n\t${MONTAGE_COMMAND} -quiet -strip -quality \"${DEFAULTCOMPRESSIONRATE}\" -mode concatenate -tile \"${TILECOLUMNS}x${TILEROWS}\" $(find \"${TILESTORAGEPATH}\" -maxdepth 1 -type f -name \"tile_tmp_*_${CLEANFILENAME##*/}.${FILEEXTENSION}\" | sort) \"${CLEANPATH}${CLEANFILENAME##*/}${OUTPUTFILESUFFIX}\".${FILEEXTENSION} >/dev/null 2>/dev/null\n\n\t# During montage reassembly, the resulting image received bytes of padding due to the way the JPEG compression algorithm works on tiles not sized as a multiple of 8\n\t# So we run jpegrescan on the final image to losslessly remove this padding and make the output JPG progressive\n\t${JPEGRESCAN_COMMAND} -q -s -i \"${CLEANPATH}${CLEANFILENAME##*/}${OUTPUTFILESUFFIX}\".${FILEEXTENSION} \"${CLEANPATH}${CLEANFILENAME##*/}${OUTPUTFILESUFFIX}\".${FILEEXTENSION}\n\n\t# Cleanup temporary files\n\trm ${TILESTORAGEPATH}${CLEANFILENAME##*/}_saliency_bw.png\n\t# We are using find to circumvent issues on Kernel based shell limitations when iterating over a large number of files with rm\n\tfind \"${TILESTORAGEPATH}\" -maxdepth 1 -type f -name \"tile_tmp_*_${CLEANFILENAME##*/}.${FILEEXTENSION}\" -exec rm {} \\;\n}\n\n# Initiate preparatory checks\nprepwork\n# If the preparations worked, launch the main program\nif (( VALIDJPEG )); then\n\tmain\nfi\n\n\n\n###############################################################################\n# EOF\n###############################################################################\n"
  },
  {
    "path": "batch-compress.sh",
    "content": "#!/bin/env bash\n\nADEPT=\"jpeg-adept.sh\"\n\nif ! which \"$ADEPT\" > /dev/null; then echo Please install $ADEPT in your path.; fi\n\nusage() {\n\techo \"Usage: $0 [Options] PATH\n\nRecursively compresses all the *.jpe?g files in PATH, \nsaving the originals.\n\nOptions (and defaults):\n\n\t-d\tmax-depth (no max directory depth)\n\t-P\tparallel (1)\n\t-n\tmax-count (no max # of filesinfinite)\t\n\t-x\tremove original images\n  \t-D\tturn on debug mode\t\n\nThe originals are saved as '_adept_save.jpg' files.\n\nThe flag file '_adept.flag' is created, so this program\ncan be run automatically.  Removal of this file will \ncause recompression of the compressed image.\n\n-x is unsafe.  It's strongly recommended to actually look at \nthe results for a while before removing the originals.   Also\nJPEG compression is lossy.   You've been warned.\n\"\n}\n\n\ndebug() {\n\tif [ $debug ]; then echo \"$@\" 1>&2; fi\n}\n\ncompress() {\n\tlocal fil=\"$1\"\n\tlocal bn=\"${fil%.*}\"\n\tif [ \"$fil\" -nt \"${bn}_adept_save.jpg\" ]; then\n\t\t# newer than both?\n\t\tif [ \"$fil\" -nt \"${bn}_adept_compress.jpg\" ]; then\n\t\tif [ \"$fil\" -nt \"${bn}_adept.flag\" ]; then\n\t\t\t# this is a new file, so compress it\n\t\t\techo + jpeg-adept.sh \\\"$fil\\\" 1>&2\n\t\t\tjpeg-adept.sh \"$fil\"\n\t\t\trm -f \"${bn}_adept.flag\"\n\t\tfi\n\t\tfi\n\n\t\t# ok, we just compressed it\n\t\tif [ \"${bn}_adept_compress.jpg\" -nt \"${bn}.jpg\" ]; then\n\t\t\t# check to see if it's smaller\n\t\t\tlocal var1=$(stat -c%s \"$fil\")\n\t\t\tlocal var2=$(stat -c%s \"${bn}_adept_compress.jpg\")\n\t\t\tdebug \"$fil: $var1, compressed: $var2\"\n\t\t\tif [ $var2 -lt $var1 ]; then\n\t\t\t\t# save original, and \n\t\t\t\tif [ $rmorig ]; then\n\t\t\t\t\tdebug \"Destroying original '$fil'\"\n\t\t\t\telse\n\t\t\t\t\tmv \"$fil\" \"${bn}_adept_save.jpg\"\n\t\t\t\tfi\n\t\t\t\tmv -f \"${bn}_adept_compress.jpg\" \"$fil\"\n\t\t\t\techo \"ok\"> \"${bn}_adept.flag\"\n\t\t\telse\n\t\t\t\techo \"toobig\"> \"${bn}_adept.flag\"\n\t\t\t\tunlink \"${bn}_adept_compress.jpg\"\n\t\t\tfi\n\t\tfi\n\tfi\n}\n\ndebug=\"\"\nunset count pll depth fil rmorig\nwhile getopts \"Dxd:f:r:n:P:\" optionName; do\ncase \"$optionName\" in\n\tP) pll=\"$OPTARG\";;\n\td) depth=\"$OPTARG\";;\n\tD) debug=1;;\n\tn) count=\"$OPTARG\";;\n\tf) fil=\"$OPTARG\";;\n\tx) rmorig=1;;\n\t\\?) usage;;\nesac\ndone\n\nunset xarg targ\nif [ $debug ]; then\n\t# turn on xtrace, xargs verbose, and pass-through -D\n\tset -o xtrace\n\txarg=\"$xarg --verbose\"\n\ttarg=\"$targ -D\"\nfi\n\nif [ $rmorig ]; then\n\ttarg=\"$targ -x\"\nfi\n\nif [ $depth ]; then\n\t# turn into a 'find' arg\n\tdepth=\" -maxdepth $depth\"\nfi\n\nif [ $pll ]; then\n\t# run commands in parallel\n\txarg=\"$xarg -P $pll\"\nfi\n\nshift `expr $OPTIND - 1`\npath=\"$1\"\n\n# inject head command\nunset head\n[ -n \"$count\" ] && head=\"head -n $count |\"\n\nif [ \"$fil\" ]; then\n\tcompress \"$fil\"\nelif [ \"$path\" ]; then\n\t# image stream\n\tfind \"$path\" $depth \\( -name '*.jpg' -or -name '*.jpeg' \\) -and -not -name '*_adept_compress.jpg' -and -not -name '*_adept_save.jpg' $farg | eval $head xargs $xarg -n 1 \"$0\" $targ -f\nelse\n\tusage \n\texit 1\nfi\n"
  },
  {
    "path": "man/man1/adept-jpeg.sh.1",
    "content": ".TH Adept 1 \"21 Dec 2018\" \"0.0.70\" \"User Manual\"\n.SH NAME\nadept-jpeg.sh \\- the adaptive JPG Compressor\n.SH DESCRIPTION\nThis script adaptively alters the compression level for areas within JPEGs (per-block)\nto achieve optimized file size while maintaining a decent visual quality.\nThis script achieves a significantly reduced file size compared to standard tools\nsuch as cjpeg while maintaining good visual quality, as can be measured via SSIM.\nThis is good news for the and thus Web Developer community to achieve\na great user experience on websites.\n.SH SYNOPSIS\n.B adept-jpeg.sh [ OPTIONS ] /path/to/image.jpeg\n.SH OPTIONS\n.TP\n.B \\-h\nDisplays help message.\n.TP\n.B \\-c string\nName compressor: imagemagick (default), cjpeg, jpegoptim, jpge, mozcjpeg.\n.TP\n.B \\-q 0..100\nCompression rate of areas considered as important, default value is 'inherit'.\n.TP\n.B \\-Q 0..100\nCompression rate of areas deemed suitable for high compression, default is 69.\n.TP\n.B \\-s suffix\nThis will append 'suffix' to filenames. Default value is '_adept_compress[_compressor]'.\n.TP\n.B \\-t integer\ntile size, accepted values are 8,16,32,64,128 and 256. The smaller the value, the better results but might take signifficantly more time to process. The default value is 'autodetect'.\n.SH COMPRESSOR\n.TP\n.B cjpeg\nStandard jpeg library.\n.TP\n.B imagemagick\nStandard jpeg library using imagemagick.\n.TP\n.B jpegoptim\nStandard jpeg library using jpegoptim.\n.TP\n.B jpge\nResearch JPEG encoder.\n.TP\n.B mozcjpeg\nMozjpeg library. A JPEG codec that provides increased compression for JPEG images.\n.SH EXAMPLE\nadept-jpeg.sh sample.jpg\n.SH SEE ALSO\n.BR imagemagick (1),\n.BR djpeg (1),\n.BR cjpeg (1),\n.BR jpegoptim (1),\n.BR jpegrescan (1),\n.BR jpge (1),\n.BR mozcjpeg (1),\n.SH AUTHOR\nCopyright (c) 2015, Tobias Baldauf.\nAll rights reserved.\n"
  },
  {
    "path": "unittests/tests_adept.bats",
    "content": "#!/usr/bin/env bats\n\nsource \"${BATS_TEST_DIRNAME}/../adept.sh\" >/dev/null 2>/dev/null\n\n@test \"Find Tools\" {\n  run find_tool IDENTIFY_COMMAND identify\n  [ $status -eq 0 ]\n}\n\n# Make sure that we actually look for tools in other directories than $PATH\n@test \"Find Tools with path resolving\" {\n  run find_tool IDENTIFY_COMMAND nonexistingcommand identify\n  [ $status -eq 0 ]\n}\n\n@test \"Validate Input JPEG\" {\n  validate_image VALIDJPEG \"test.jpg\"\n  result=${VALIDJPEG}\n  [ \"$result\" -eq 1 ]\n}\n\n@test \"Read Image Dimension\" {\n  find_image_dimension IMAGEWIDTH \"test.jpg\" 'w'\n  result=${IMAGEWIDTH}\n  [ \"$result\" -eq 512 ]\n}\n\n@test \"Optimize Tile Size\" {\n  optimize_tile_size TILESIZE 'autodetect' 513 513\n  result=${TILESIZE}\n  [ \"$result\" -eq 16 ]\n}\n\n@test \"Slice Image into Tiles\" {\n  CLEANFILENAME='test'\n  FILEEXTENSION='jpg'\n  slice_image_to_ram \"test.jpg\" 32 \"$BATS_TMPDIR/\"\n  TILESARRAY=($(find \"$BATS_TMPDIR/\" -maxdepth 1 -iregex \".*$CLEANFILENAME.jpe*g\"))\n  result=${#TILESARRAY[@]}\n  [ \"$result\" -eq 256 ]\n  rm -f \"$BATS_TMPDIR/.*.jpe*g\"\n}\n\n@test \"Calculate Tile Count for Reassembly\" {\n  calculate_tile_count TILEROWS 512 32\n  result=${TILEROWS}\n  [ \"$result\" -eq 16 ]\n}\n\n# NEEDS IMPLEMENTING: optimize_salient_regions_amount\n# NEEDS REFACTORING: reassemble_tiles_into_final_image\n# NEEDS REFACTORING: estimate_tile_content_complexity_and_compress\n# Uses globals ${TILESTORAGEPATH} and ${FILEEXTENSION} etc. Replace with locals.\n# Currently has two steps: recompile + recompress. This should be two seperate functions.\n"
  }
]