#!/usr/local/bin/ksh

######################################################################
# Program:	mkthumbnail
# Purpose:	Create thumbnails from images, and
#		generate supporting HTML.
# Copyright:    Copyright 1997-2026 Perette Barella
#               All rights reserved.
#---------------------------------------------------------------------
VERSION='$Id: mkthumbnails 385 2026-04-20 14:40:48Z perette $'

# dir_name - dirname implemented as a shell function.
# Author: Perette Barella
# Copyright 2020 Devious Fish.  All rights reserved.
# Why: In testing, performed >> 10x faster than external program.
# $Id$

function dir_name {
        typeset dir=${1%/*}
        [[ "$1" = "$dir" ]] && dir="."
        print -r -- "$dir"
}

# base_name - basename implemented as a shell function.
# Author: Perette Barella
# Copyright 2020 Devious Fish.  All rights reserved.
# Why: In testing, performed >> 10x faster than external program.
# $Id$

function base_name {
        typeset base=${1##*/}
        (( $# == 2 )) && eval "base=\${base%$2}"
        print -r -- "$base"
}

# Validate that ksh supports the modern/extended getopts format.
# Author: Perette Barella 
# Copyright 2018 Devious Fish.  All rights reserved.
# $Id: modern_ksh_check 19 2018-07-28 23:40:39Z perette $  


function modern_ksh_check {
	if [[ $(getopts '[-][12:abc]' flag --abc; print -- 0$flag) != "012" ]]
	then
		print -- "$arg0${arg0+: }Outdated Korn shell." 1>&2
		exit 1
	fi
}
# Escape HTML entities
# Author: Perette Barella
# Copyright 2020 Devious Fish.  All rights reserved.
# $Id: html_escape_entities 56 2020-01-04 01:21:35Z perette $

function html_escape_entities {
	if (($# == 0))
	then
		sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g'
	else
		print -r -- "$*" | html_escape_entities
	fi
}


# "Prime" autoloaded functions used in command substitutions
dir_name . >/dev/null
base_name . >/dev/null
html_escape_entities no-op >/dev/null

arg0=$(base_name $0)

modern_ksh_check

USAGE=$'
[-1?'$VERSION$']
[+NAME?'$arg0$' - make thumbnails and generate supporting HTML snippets.]
[+DESCRIPTION?\b'$arg0$'\b creates or updates thumbnails that conform to given size limits.]
[+?Missing or outdated thumbnails (those older than their corresponding original) are always updated.  By default, \b'$arg0$'\b also checks the thumbnail size against the requested size, and will update if the requested size is different, ensuring thumbnails sizes are kept compliant with page expectations.  The \b-f\b option and \vMKTHUMBNAILS_FAST\v environment variables disable this check.]
[+?Additionally, \b'$arg0$'\b also outputs HTML (or macro calls that will translate HTML) to reference the thumbnail.]
[d:description?Specify a description as an accessibility alternative.]:[alt-text]
[f:fast?Fast and flexible: don\'t validate existing thumbnail size.  Additionally, if a thumbnail would be larger than the original, just hard link the original.]
[l:link?Wrap images in links to the image files.]
[m:media?Output a source media entry for referencing the thumbnail through a \b<PICTURE>\b element.]
[p:path?Specify a path for thumbnails.  Default is \bThumbnails\b.]:[directory]
[q:quality?Specify image quality, where 1=poor, 99=best.]#[quality]
[s:single?Create a single image, with the specified HTML attributes.]:[attributes]
[v:verbose?Generate messages about thumbnail decisions.]
[+ENVIRONMENT?]
{
  [+MKTHUMBNAILS_FAST?If set to \btrue\b is equivalent to the \b-f\b flag.]
  [+MKTHUMBNAILS_VERBOSE?If set to \btrue\b is equivalent to the \b-v\b flag.]
}
[+SEE ALSO?\bweb2html\b(1)]

max-width max-height files ...

[-author?Perette Barella <perette@deviousfish.com>]
'






######################################################################
# Function:	resize_this
# Purpose:	Calculates the destination size given the actual
#		image sizes.  Generates the HTML.
# Arguments:	$1 - width
#		$2 - height
#		$3 - filename
# Returns:	0 on success, non-0 on failure.
# Author:	Perette Barella
#---------------------------------------------------------------------
function resize_this
{
	typeset -i x="$1" y="$2"
	typeset file="$3" thumbfile="$4" alternate=""
	# calculate the adjustment for width and height as a per thousand.
	typeset -F adjwidth=9999
	typeset -F adjheight=9999
	[ "$DESTWIDTH" != "-" ] && let adjwidth="float(DESTWIDTH) / $x"
	[ "$DESTHEIGHT" != "-" ] && let adjheight="float(DESTHEIGHT) / $y"

	typeset -F adjust=adjwidth
	(( adjheight < adjwidth )) && let adjust=adjheight
	let newwidth="round(x * adjust)"
	let newheight="round(y * adjust)"
	if $MEDIA
	then
		print -- "<source media='(max-width:${newwidth}px)' srcset='$thumbfile'>"
	else
		if ! $SINGLE
		then
			typeset alternate=""
			if ! expr "$file" : "\(.*/\)\{0,\}[A-Z0-9]\{0,3\}[0-9][0-9]*\.[a-z]*\$" >/dev/null
			then
				alternate=$(print -- "$file" | html_escape_entities)
			fi
			[ "$alternate" = "" ] && alternate="Thumbnail."
			EXTRAATTRS="TITLE=\"${alternate//\"/}\""
			print -n "<li>"
		fi
		$LINK && print -- "<A HREF=\"${file}\">"
		print -- "_IMAGE(\`\`$thumbfile'', \`\`${DESCRIPTION:-$alternate}'',,, \`\`$EXTRAATTRS'')"
		$LINK && print -- "</A>"
	fi
	return 0
}
##### End of function resize_this #####




######################################################################
# Function:	resize_jpeg
# Purpose:	Resize a jpeg (making a thumbnail) and write out the new file.
# Arguments:	$1 - the file to resize.
# Returns:	0 on success, non-0 on error
# Author:	Perette Barella
#---------------------------------------------------------------------
function resize_jpeg
{
	typeset file="$1"
	typeset dirname=$(dir_name "$file")
        typeset thumbpath="$dirname/$THUMBNAILPATH"
	typeset thumbfile="$thumbpath/$(base_name "$file")"

	if $FAST && [[ -f $file && ( $thumbfile -nt $file || $thumbfile -ef $file ) ]]
	then
		$VERBOSE && print -- "Using existing $thumbfile (-f fast mode)" 1>&2
		return 0
	fi

	mkdir -p "$thumbpath" || return 1
	set $(identify -auto-orient -format "%w %h" "$file") || exit 1

	if ! typeset -i x="$1" || ! typeset -i y="$2"
	then
		print -- "$arg0: width or height of $file screwed up: $x*$y" 1>&2
		return 1
	fi

	resize_this "$x" "$y" "$file" "$thumbfile" || return $?
	if [[ -f "$thumbfile" && "$thumbfile" -nt "$file" ]]
	then
		if set $(identify -auto-orient -format "%w %h" "$thumbfile")
		then
			typeset -i thumb_x="$1"
			typeset -i thumb_y="$2"
			# The size may be off by +1.  This seems to be
			# the convert utility trying to maintain proportions.
			typeset -i margin=2
			if (( thumb_x >= (newwidth - margin) &&
			      thumb_x <= (newwidth + margin) &&
			      thumb_y >= (newheight - margin) &&
			      thumb_y <= (newheight + margin) ))
			then
				$VERBOSE && print -- "Using existing $thumbfile" 1>&2
				return 0
			fi
		fi
		$VERBOSE && print "Resizing $thumbfile" 1>&2
	else
		$VERBOSE && print "Creating $thumbfile" 1>&2
	fi

	# If it's hard-linked to the original, delete the thumbnail.
	[[ $file -ef $thumbfile ]] && rm -f "$thumbfile"
	convert -auto-orient -quality $QUALITY -strip -geometry ${newwidth}x${newheight} "$file" "$thumbfile"
	if [[ $file == *.png ]] && whence -q pngcrush
	then
		pngcrush -q -brute "$thumbfile" "$thumbfile.smaller" &&
			mv "$thumbfile.smaller" "$thumbfile"
	fi
	if $FAST && (( $(wc -c < "$file") < $(wc -c < "$thumbfile") ))
	then
		$VERBOSE && print -- "Thumbnail is larger than original, hard linking files." 1>&2
		ln -f "$file" "$thumbfile"
	fi
	return $?
}
##### End of function resize_jpeg #####



##### Start of main #####

typeset option
SINGLE=false
DESCRIPTION=""
EXTRAATTRS=""
LINK=false
MEDIA=false
QUALITY=75
THUMBNAILPATH="Thumbnails"
FAST="${MKTHUMBNAILS_FAST:-false}"
[[ "$FAST" != "true" ]] && FAST=false
VERBOSE="${MKTHUMBNAILS_VERBOSE:-false}"
[[ "$VERBOSE" != "true" ]] && VERBOSE=false

while getopts -a "$arg0" "$USAGE" option
do
    case "$option" in
	d)	DESCRIPTION="$OPTARG" ;;
	f)	FAST=true ;;
	l)	LINK=true ;;
	m)	MEDIA=true ;;
	p)	THUMBNAILPATH="$OPTARG" ;;
	q)	QUALITY="$OPTARG"
		if ((QUALITY < 1 || QUALITY > 99))
		then
			print "$arg0: Quality must be in range 1-99." 1>&2
			exit 1
		fi ;;
	s)	SINGLE=true
		EXTRAATTRS="$OPTARG" ;;
	v)	VERBOSE=true ;;
    esac
done
shift $((OPTIND - 1))

typeset -r DESTWIDTH="$1"
typeset -r DESTHEIGHT="$2"
(( $# < 2 )) && getopts -a "$arg0" "$USAGE" option "-?"
shift 2

status=0

for file in "$@"
do
	if [ ! -e "$file" ]
	then
		print -- "$arg0: $file does not exist" 1>&2
		status=1
		continue
	fi

	case "$file" in
		*/Thumbnails/*|Thumbnails/*)
			# Silently skip things that are already thumbnails.
			: ;;
		*)
			# ImageMagick handles most formats -- just pass
			# everything off to it.
			resize_jpeg $file
			status=$?
			;;
	esac
done

exit $status



##### End of main #####

