Skip to content

Instantly share code, notes, and snippets.

@alexmarkley
Last active September 1, 2020 18:48
Show Gist options
  • Save alexmarkley/696e9788e61995e5f0a936cc33faa2b9 to your computer and use it in GitHub Desktop.
Save alexmarkley/696e9788e61995e5f0a936cc33faa2b9 to your computer and use it in GitHub Desktop.
generateImageDerivatives.sh -- That Feeling When You Realize You Should Have Used A Higher-Level Language But You've Already Written Too Much Bash And You Can't Stop Now...
#!/bin/bash
set -o pipefail
# Requested image sizes describes the size breakpoints we will resize down to. Sizes larger than the input image are ignored.
REQUESTED_IMAGE_SIZES=(3840 2880 1920 1440 1280 1024 640 320 128 64)
# Manifest format version should be incremented only if the format changes enough that a client should not try to parse it.
MANIFEST_FORMAT_VERSION=1
# Manifest generation version should be incremented every time the derivative generation rules change (for example if REQUESTED_IMAGE_SIZES is updated or if the resampling algorithm changes) to force a regeneration of the derivatives even if the source image has not changed.
MANIFEST_GENERATION_VERSION=1
MYTEMPDIR=$(mktemp -d)
function _cleanup() {
if [ ! -z "${MYTEMPDIR}" ]; then
rm -rf "${MYTEMPDIR}"
fi
}
function _log() {
echo generateImageDerivatives.sh: "${@}" 1>&2
}
function _die() {
_log FATAL: "${@}"
_cleanup
exit 1
}
BASEPATH="$( cd "$(dirname "${0}")/.." && pwd -P )" || _die "Failed to identify BASEPATH."
# _log "Base Path: ${BASEPATH}"
CONTENTPATH="${BASEPATH}/content/"
# _log "Content Path: ${CONTENTPATH}"
REALPATH="$(which realpath)"
if [ -z "${REALPATH}" ]; then
REALPATH="$(which grealpath)"
if [ -z "${REALPATH}" ]; then
_die "Could not find realpath binary."
fi
fi
SHA256SUM="$(which sha256sum)"
if [ -z "${SHA256SUM}" ]; then
SHA256SUM="$(which gsha256sum)"
if [ -z "${SHA256SUM}" ]; then
_die "Could not find sha256sum binary."
fi
fi
if [ -z "${DESTINATION_BUCKET}" ]; then
_die "Missing required environment variable DESTINATION_BUCKET."
fi
INPUT_IMAGE="${1}"
_log "Checking Input Image: ${INPUT_IMAGE}"
INPUT_IMAGE_FULLPATH="$("${REALPATH}" "${INPUT_IMAGE}")" || _die "Could not find the image you asked for."
# _log "Input Image Full Path: ${INPUT_IMAGE_FULLPATH}"
INPUT_IMAGE_PATHPREFIX="${INPUT_IMAGE_FULLPATH:0:${#CONTENTPATH}}"
if [ "${CONTENTPATH}" != "${INPUT_IMAGE_PATHPREFIX}" ]; then
_die "Input Image must exist within the Content Path: ${CONTENTPATH}"
fi
CONTENT_KEY="${INPUT_IMAGE_FULLPATH:${#CONTENTPATH}}"
_log "Image Content Key: ${CONTENT_KEY}"
INPUT_IMAGE_SHA256SUM=$("${SHA256SUM}" "${INPUT_IMAGE_FULLPATH}" | cut -f1 -d' ') || _die "Failed to calculate image sha256sum"
if [ -z "${INPUT_IMAGE_SHA256SUM}" ]; then
_die "Image sha256sum cannot be an empty string"
fi
_log "Calculated image sha256sum: ${INPUT_IMAGE_SHA256SUM}"
INPUT_IMAGE_ATTRIBUTES=$(identify -format "%[w] %[h] %[e]" "${INPUT_IMAGE_FULLPATH}[0]") || _die "Failed to identify input image"
INPUT_IMAGE_WIDTH=$(echo "${INPUT_IMAGE_ATTRIBUTES}" | cut -f1 -d' ') || _die "Failed to parse input image width"
if ! [ "${INPUT_IMAGE_WIDTH}" -eq "${INPUT_IMAGE_WIDTH}" ] 2>/dev/null; then
_die "input image width is apparently not an integer"
fi
INPUT_IMAGE_HEIGHT=$(echo "${INPUT_IMAGE_ATTRIBUTES}" | cut -f2 -d' ') || _die "Failed to parse input image height"
if ! [ "${INPUT_IMAGE_HEIGHT}" -eq "${INPUT_IMAGE_HEIGHT}" ] 2>/dev/null; then
_die "input image height is apparently not an integer"
fi
INPUT_IMAGE_EXTENSION=$(echo "${INPUT_IMAGE_ATTRIBUTES}" | cut -f3 -d' ') || _die "Failed to parse input image extension"
if [ -z "${INPUT_IMAGE_EXTENSION}" ]; then
_die "input image apparently has no extension"
fi
_log "Image has extension \"${INPUT_IMAGE_EXTENSION}\" and size ${INPUT_IMAGE_WIDTH} x ${INPUT_IMAGE_HEIGHT}"
IMAGE_PATHCOMPONENTS_DIR=$(dirname "${CONTENT_KEY}") || _die "dirname failed"
IMAGE_PATHCOMPONENTS_BASE=$(basename "${CONTENT_KEY}" "${INPUT_IMAGE_EXTENSION}") || _die "basename failed"
IMAGE_PATHCOMPONENTS_EXTENSION="${INPUT_IMAGE_EXTENSION}"
MANIFEST_FILENAME="${IMAGE_PATHCOMPONENTS_BASE}${IMAGE_PATHCOMPONENTS_EXTENSION}.manifest.json"
MANIFEST_FILEPATH_LOCAL="${MYTEMPDIR}/${MANIFEST_FILENAME}"
MANIFEST_S3_URL="s3://${DESTINATION_BUCKET}/derivatives/manifest/${IMAGE_PATHCOMPONENTS_DIR}/${MANIFEST_FILENAME}"
_log "Checking for existing manifest for this image..."
aws s3 cp "${MANIFEST_S3_URL}" "${MANIFEST_FILEPATH_LOCAL}"
FETCHED="${?}"
if [ "${FETCHED}" -eq 0 ]; then
EXISTINGMANIFEST_FORMAT_VERSION=$(cat "${MANIFEST_FILEPATH_LOCAL}" | jq -r '.formatVersion')
EXISTINGMANIFEST_GENERATION_VERSION=$(cat "${MANIFEST_FILEPATH_LOCAL}" | jq -r '.generationVersion')
EXISTINGMANIFEST_CONTENT_KEY=$(cat "${MANIFEST_FILEPATH_LOCAL}" | jq -r '.contentKey')
EXISTINGMANIFEST_SOURCE_SHA256SUM=$(cat "${MANIFEST_FILEPATH_LOCAL}" | jq -r '.sourceSHA256SUM')
_log "Existing manifest has formatVersion: ${EXISTINGMANIFEST_FORMAT_VERSION}, generationVersion: ${EXISTINGMANIFEST_GENERATION_VERSION}, contentKey: ${EXISTINGMANIFEST_CONTENT_KEY}, sourceSHA256SUM: ${EXISTINGMANIFEST_SOURCE_SHA256SUM}"
if [ "${EXISTINGMANIFEST_FORMAT_VERSION}" = "${MANIFEST_FORMAT_VERSION}" -a "${EXISTINGMANIFEST_GENERATION_VERSION}" = "${MANIFEST_GENERATION_VERSION}" -a "${EXISTINGMANIFEST_CONTENT_KEY}" = "${CONTENT_KEY}" -a "${EXISTINGMANIFEST_SOURCE_SHA256SUM}" = "${INPUT_IMAGE_SHA256SUM}" ]; then
_log "Nothing has changed with this image, declining to update derivatives."
_cleanup
exit 0
else
_log "Existing manifest file has properties that don't match. Going ahead with creating derivatives..."
fi
else
_log "No existing manifest file. Going ahead with creating derivatives..."
fi
IMAGE_LONGEST_LENGTH=""
if [ "${INPUT_IMAGE_WIDTH}" -ge "${INPUT_IMAGE_HEIGHT}" ]; then
IMAGE_LONGEST_LENGTH="${INPUT_IMAGE_WIDTH}"
_log "Input image's longest dimension is width: ${IMAGE_LONGEST_LENGTH}"
else
IMAGE_LONGEST_LENGTH="${INPUT_IMAGE_HEIGHT}"
_log "Input image's longest dimension is height: ${IMAGE_LONGEST_LENGTH}"
fi
DERIVATIVEIMAGE_JSONTEMPLATE='{key: $key, isOriginal: $isOriginal, longestSide: $longestSide, width: $width, height: $height}'
ORIGINAL_IMAGE_JSON=$(jq -n -r --arg key "content/${CONTENT_KEY}" --argjson isOriginal true --argjson longestSide "${IMAGE_LONGEST_LENGTH}" --argjson width "${INPUT_IMAGE_WIDTH}" --argjson height "${INPUT_IMAGE_HEIGHT}" "${DERIVATIVEIMAGE_JSONTEMPLATE}") || _die "failed to generate original image json"
IMAGES_JSON=("${ORIGINAL_IMAGE_JSON}")
for SIZE in "${REQUESTED_IMAGE_SIZES[@]}"; do
_log "Handling image size ${SIZE}..."
if [ "${SIZE}" -lt "${IMAGE_LONGEST_LENGTH}" ]; then
OUTPUT_FILENAME="${IMAGE_PATHCOMPONENTS_BASE}${SIZE}.${IMAGE_PATHCOMPONENTS_EXTENSION}"
OUTPUT_FILEPATH_LOCAL="${MYTEMPDIR}/${OUTPUT_FILENAME}"
OUTPUT_S3_KEY="derivatives/generatedImages/${IMAGE_PATHCOMPONENTS_DIR}/${OUTPUT_FILENAME}"
OUTPUT_S3_URL="s3://${DESTINATION_BUCKET}/${OUTPUT_S3_KEY}"
_log "Generating resized [ fit to ${SIZE}x${SIZE} ] image: ${OUTPUT_FILEPATH_LOCAL}"
convert "${INPUT_IMAGE_FULLPATH}" -resize "${SIZE}"x"${SIZE}" "${OUTPUT_FILEPATH_LOCAL}" || _die "Failed to generate output image."
OUTPUT_IMAGE_ATTRIBUTES=$(identify -format "%[w] %[h]" "${OUTPUT_FILEPATH_LOCAL}[0]") || _die "Failed to identify output image"
OUTPUT_IMAGE_WIDTH=$(echo "${OUTPUT_IMAGE_ATTRIBUTES}" | cut -f1 -d' ') || _die "Failed to parse output image width"
if ! [ "${OUTPUT_IMAGE_WIDTH}" -eq "${OUTPUT_IMAGE_WIDTH}" ] 2>/dev/null; then
_die "output image width is apparently not an integer"
fi
OUTPUT_IMAGE_HEIGHT=$(echo "${OUTPUT_IMAGE_ATTRIBUTES}" | cut -f2 -d' ') || _die "Failed to parse output image height"
if ! [ "${OUTPUT_IMAGE_HEIGHT}" -eq "${OUTPUT_IMAGE_HEIGHT}" ] 2>/dev/null; then
_die "output image height is apparently not an integer"
fi
_log "Uploading generated image: ${OUTPUT_S3_URL}"
aws s3 cp "${OUTPUT_FILEPATH_LOCAL}" "${OUTPUT_S3_URL}" || _die "Failed to upload generated image."
DERIV_IMAGE_JSON=$(jq -n -r --arg key "${OUTPUT_S3_KEY}" --argjson isOriginal false --argjson longestSide "${SIZE}" --argjson width "${OUTPUT_IMAGE_WIDTH}" --argjson height "${OUTPUT_IMAGE_HEIGHT}" "${DERIVATIVEIMAGE_JSONTEMPLATE}") || _die "Failed to generate derivative image json"
IMAGES_JSON+=("${DERIV_IMAGE_JSON}")
else
_log "Ignoring this size, because it would result in upsampling."
fi
done
echo "${IMAGES_JSON[@]}" | jq -r -s --argjson formatVersion "${MANIFEST_FORMAT_VERSION}" --argjson generationVersion "${MANIFEST_GENERATION_VERSION}" --arg contentKey "${CONTENT_KEY}" --arg sourceSHA256SUM "${INPUT_IMAGE_SHA256SUM}" '{ formatVersion: $formatVersion, generationVersion: $generationVersion, contentKey: $contentKey, sourceSHA256SUM: $sourceSHA256SUM, variants: . }' >"${MANIFEST_FILEPATH_LOCAL}" || _die "failed to generate manifest json"
_log "Uploading manifest file: ${MANIFEST_S3_URL}"
aws s3 cp "${MANIFEST_FILEPATH_LOCAL}" "${MANIFEST_S3_URL}" || _die "failed to upload manifest json"
_cleanup
_log "All done."
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment