#!/bin/bash # # $Rev$ $Date$ # # vcs # Video Contact Sheet *NIX: Generates contact sheets (previews) of videos # # Copyright (C) 2007, 2008 Toni Corvera # with patches from Phil Grundig and suggestions/corrections from # many others (see homepage) # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # # Author: Toni Corvera # # References: # Pages I've taken snippets from or wrote code based on them. # I refer to them in comments as e.g. [[R1]]. [[R1#3]] Means section 3 in R1. # I also use internal references in the form [x1] (anchor -where to point-) # and [[x1]] (x-reference -point to what-). # [R0] getopt-parse.bash example, on Debian systems: # /usr/share/doc/util-linux/examples/getopt-parse.bash.gz # [R1] Bash (and other shells) tips # # [R2] List of officially registered FOURCCs and WAVE Formats (aka wFormatTag) # # [R3] Unofficial list of FOURCCs # # [R4] A php module with a list of FOURCCs and wFormatTag's mappings # # [M1] "[MEncoder-users] Thumbnail creation" # # [VC1] VC-1 and derived codecs information # # declare -r VERSION="1.0.11" # {{{ # CHANGELOG # History (The full changelog can be found at ). # # 1.0.11: (2008-04-08) # * BUGFIX: (brown bag bug) Corrected typo in variable name that made vcs # fail when setting the default timecode derivation to number of # captures instead of interval (i.e. when including timecode_from=8 in # the config file) (thanks to Chris Hills for the bug report) # * WORKAROUND: Fix for all-equal captures (seems to be a known problem # with mplayer [M1]) (contributed by Phil Grundig) # * RETROCOMPATIBILITY: Older bash syntax for appending and initialising # arrays (contributed by Phil Grundig) # * COMPATIBILITY: Support alternative du syntax for compatibility with # busybox (based on Phil Grundig's contribution) # * COSMETIC: Don't print milliseconds when using mplayer as capturer # (they're not really meaningful then) (suggested by Phil Grundig) # * COSMETIC: Align the extended set captures (-e) and the standard set # (bug pointed by Chris Hills). Seems to fail at some (smaller?) # sizes. # "Funky" modes aren't correctly aligned yet. # * DEBUGGING: Added optional function call trace (by setting variable DEBUG # to 1) # * Added FOURCC for VC-1 # * COSMETIC: Fixed captures recount with multiple files (prompted by a # bugreport from Dougn Redhammer) # }}} # CHANGELOG set -e # {{{ # TODO # TODO / FIXME: # * [[R1#22]] states that not all bc versions understand '<', more info required # * [[x1]] Find out why the order of ffmpeg arguments breaks some files. # * [[x2]] Find out if egrep is safe to use or grep -E is more commonplace. # # }}} # TODO # {{{ # Constants # Configuration file, please, use this file to modify the behaviour of the # script. Using this allows overriding some variables (see below) # to your liking. Only lines with a variable assignment are evaluated, # it should follow bash syntax, note though that ';' can't be used # currently in the variable values; e.g.: # # # Sample configuration for vcs # user=myname # Sign all compositions as myname # bg_heading=gray # Make the heading gray # # There is a total of three configuration files than are loaded if the exist: # * /etc/vcs.conf: System wide conf, least precedence # * $CFGFILE (by default ~/.vcs.conf): Per-user conf, second least precedence # * ./vcs.conf: Per-dir confif, most precedence # # The variables that can be overriden are below the block of constants ahead. declare -r CFGFILE=~/.vcs.conf # see $decoder declare -ri DEC_MPLAYER=1 DEC_FFMPEG=3 # See $timecode_from declare -ri TC_INTERVAL=4 TC_NUMCAPS=8 # These can't be overriden, modify this line if you feel the need declare -r PROGRAM_SIGNATURE="Video Contact Sheet *NIX ${VERSION} " # see $safe_rename_pattern declare -r DEFAULT_SAFE_REN_PATT="%b-%N.%e" # see $extended_factor declare -ri DEFAULT_EXT_FACTOR=4 # see $verbosity declare -ri V_ALL=5 V_NONE=-1 V_ERROR=1 V_WARN=2 V_INFO=3 # see $font_filename and $FONT_MINCHO declare -ri FF_DEFAULT=5 FF_MINCHO=7 # Indexes in $VID declare -ri W=0 H=1 FPS=2 LEN=3 VCODEC=4 ACODEC=5 VDEC=6 CHANS=7 # Exit codes, same numbers as /usr/include/sysexits.h declare -r EX_OK=0 EX_USAGE=64 EX_UNAVAILABLE=69 \ EX_NOINPUT=66 EX_SOFTWARE=70 EX_CANTCREAT=73 \ EX_INTERRUPTED=79 # This one is not on sysexits.h # The context allows the creator to identify which contact sheet it is creating # (CTX_*) HL: Highlight (-l), STD: Normal, EXT: Extended (-e) declare -ri CTX_HL=1 CTX_STD=2 CTX_EXT=3 # This is the horizontal padding added to each capture. Changing it might break # extended set's alignement so keep this in mind if you tinker with it declare -ri HPAD=8 # }}} # End of constants # {{{ # Override-able variables # Set to 1 to print function calls declare -i DEBUG=0 declare -i DEFAULT_INTERVAL=300 declare -i DEFAULT_NUMCAPS=16 declare -i DEFAULT_COLS=2 # Text before the user name in the signature declare user_signature="Preview created by" # By default sign as the system's username (see -u, -U) declare user=$(id -un) # Which of the two methods should be used to guess the number of thumbnails declare -i timecode_from=$TC_INTERVAL # Which of the two vidcappers should be used (see -F, -M) # mplayer seems to fail for mpeg or WMV9 files, at least on my system # also, ffmpeg allows better seeking: ffmpeg allows exact second.fraction # seeking while mplayer apparently only seeks to nearest keyframe declare -i decoder=$DEC_FFMPEG # Options used in imagemagick, these options set the final aspect # of the contact sheet declare output_format=png # ImageMagick decides the type from the extension declare -i output_quality=92 # Output image quality (only affects the final # image and obviously only in lossy formats) # Colours, see convert -list color to get the list declare bg_heading=YellowGreen # Background for meta info (size, codec...) declare bg_sign=SlateGray # Background for signature declare bg_title=White # Background for the title (see -T) declare bg_contact=White # Background of the thumbnails declare fg_heading=black # Font colour for meta info box declare fg_sign=black # Font colour for signature declare fg_tstamps=white # Font colour for timestamps declare fg_title=Black # Font colour fot the title # Fonts, see convert -list type to get the list declare font_tstamps=courier # Used for timestamps over the thumbnails declare font_heading=helvetica # Used for the heading (meta info box) declare font_sign=$font_heading # Used for the signature box # Unlike other font_ variables this doesn't take a font name directly # but is restricted to the $FF_ values. This is to allow overrides # from the command line to be placed anywhere, i.e. in # $ vcs -I file.avi -O 'FONT_MINCHO=whatever' # as the font is overridden is after requesting its use, it wouldn't be # affected # The other font_ variables are only affected by overrides and not command # line options that's why this one is special. declare font_filename=$FF_DEFAULT # Used to print only the filename in the heading declare font_title=$font_heading # Used fot the title (see -T) # Font sizes, in points declare -i pts_tstamps=18 # Used for the timestamps declare -i pts_meta=16 # Used for the meta info box declare -i pts_sign=11 # Used for the signature declare -i pts_title=36 # Used for the title (see -T) # See --shoehorn declare shoehorned= # See -E / $end_offset declare -i DEFAULT_END_OFFSET=60 # This can only be changed in the configuration file # Change it to change the safe renanimg: # When writing the output file, the input name + output extension is # used (e.g.: "some video.avi.png"), if it already exists, though, # a number if appended to the name. This variable dictates where the number is # placed. # By default "%b-%N.%e" where: # %b is the basename (file name without extension) # %N is the appended number # %e is the extension # The default creates outputs like "output.avi-1.png" # # If overridden with an incorrect value it will be silently set to the default declare safe_rename_pattern="$DEFAULT_SAFE_REN_PATT" # Controls how many extra captures will be created in the extended mode # (see -e), 0 is the same as disabling the extended mode # This number is multiplied by the total number of captures to get # the number of extra captures. So, e.g. -n2 -e2 leads to 4 extra captures. declare extended_factor=0 # Options added always to the ones in the command line # (command line options override them). # Note using this is a bit tricky :P mostly because I've no clue of how this # should be done. # As an example: you want to set always the title to "My Title" and output # to jpeg: default_options="-T'My Title' -j" #declare default_options= # Verbosity level so far from the command line can only be muted (see -q) # it can be overridden, though declare -i verbosity=$V_ALL # When set to 0 the status messages printed by vcs while running # are coloured if the terminal supports it. Set to 1 if this annoys you. declare -i plain_messages=0 # Experimental in 1.0.7b: # Experiment to get international font support # I'll need to get some help here, so if you use anything beyond a latin # alphabet, please help me choosing the correct fonts # To my understanding Ming/Minchō fonts should cover most of Japanse, # Chinese and Korean # Apparently Kochi Mincho should include Hangul *and* Cyrillic... which would be # great :) Although it couldn't write my hangul test, and also the default font # (helvetica) in my system seems to include cyrillic too, or at least a subset of # it. declare FONT_MINCHO=/usr/share/fonts/truetype/kochi/kochi-mincho.ttf # Output of capturing programs is redirected here declare stdout=/dev/null stderr=/dev/null # }}} # End of override-able variables # {{{ # Variables # Options and other internal usage variables, no need to mess with this! declare interval=$DEFAULT_INTERVAL # Interval of captures (=numsecs/numcaps) declare -i numcaps=$DEFAULT_NUMCAPS # Number of captures (=numsecs/interval) declare title="" declare fromtime=0 # Starting second (see -f) declare totime=-1 # Ending second (see -t) declare -a initial_stamps # Manually added stamps (see -S) declare -i th_height= # Height of the thumbnails, by default use same as input declare -i cols=$DEFAULT_COLS # Number of output columns declare -i manual_mode=0 # if 1, only command line timestamps will be used declare aspect_ratio=0 # If 0 no transformations done (see -a) # If -1 try to guess (see -A) declare -a TEMPSTUFF # Temporal files declare -a TIMECODES # Timestamps of the video captures declare -a HLTIMECODES # Timestamps of the highlights (see -l) declare VCSTEMPDIR= # Temporal directory, all temporal files # go there # This holds the output of mplayer -identify on the current video declare MPLAYER_CACHE= # This holds the parsed values of MPLAYER_CACHE, see also the Indexes in VID # (defined in the constants block) declare -a VID= # Workarounds: # Argument order in FFmpeg is important -ss before or after -i will make # the capture work or not depending on the file. See -Wo. # TODO: [x1]. # Admittedly the workaraound is abit obscure: those variables will be added to # the ffmpeg arguments, before and after -i, replacing spaces by the timestamp. # e.g.: for second 50 '-ss ' will become '-ss 50' while '' will stay empty # By default -ss goes before -i. declare wa_ss_af="" wa_ss_be="-ss " # This number of seconds is *not* captured from the end of the video declare -i end_offset=$DEFAULT_END_OFFSET # Experimental in 1.0.7b: transformations/filters # Operations are decomposed into independent optional steps, this will allow # to add some intermediate steps (e.g. polaroid mode) # Filters in this context are functions. # There're two kinds of filters and a delegate: # * individual filters are run over each vidcap # * global filters are run over all vidcaps at once # * The contact sheet creator delegates on some function to create the actual # contact sheet # # Individual filters take the form: # filt_name( vidcapfile, timestamp in seconds.milliseconds, width, height ) # They're executed in order by filter_vidcap() declare -a FILTERS_IND=( 'filt_resize' 'filt_apply_stamp' ) # Global filters take the form # filtall_name( vidcapfile1, vidcapfile2, ... ) # They're executed in order by filter_all_vidcaps declare -a FILTERS_CS # The contact sheet creators take the form # csheet_name( number of columns, context, width, height, vidcapfile1, # vidcapfile2, ... ) : outputfile # Context is one of the CTX_* constants (see below) # The width and height are those of an individual capture # It is executed by create_contact_sheet() declare CSHEET_DELEGATE=csheet_montage # Gravity of the timestamp (will be override-able in the future) declare grav_timestamp=SouthEast # When set to 1 the signature won't contain the "Preview created by..." line declare -i anonymous_mode=0 # See csheet_montage for more details declare -i DISABLE_SHADOWS=0 # }}} # Variables # {{{ # Configuration handling # These are the variables allowed to be overriden in the config file, # please. # They're REGEXes, they'll be concatenated to form a regex like # (override1|override2|...). # Don't mess with this unless you're pretty sure of what you're doing. # All this extra complexity is done to avoid including the config # file directly for security reasons. declare -ra ALLOWED_OVERRIDES=( 'user' 'user_signature' 'bg_.*' 'font_.*' 'pts_.*' 'fg_.*' 'output_quality' 'DEFAULT_INTERVAL' 'DEFAULT_NUMCAPS' 'DEFAULT_COLS' 'decoder' 'output_format' 'shoehorned' 'timecode_from' 'safe_rename_pattern' # 'default_options' 'extended_factor' 'verbosity' 'plain_messages' 'FONT_MINCHO' 'stdout' 'stderr' 'DEFAULT_END_OFFSET' 'DEBUG' ) # This is only used to exit when -DD is used declare -i DEBUGGED=0 # It will be 1 after using -D # Loads the configuration files if present # load_config() load_config() { local CONFIGS=( /etc/vcs.conf $CFGFILE ./vcs.conf) for cfgfile in ${CONFIGS[*]} ;do if [ ! -f "$cfgfile" ]; then continue; fi while read line ; do # auto variable $line override "$line" "file $cfgfile" # Feeding it comments should be harmless done <$cfgfile done # Override-able hack, this won't work with command line overrides, though end_offset=$DEFAULT_END_OFFSET interval=$DEFAULT_INTERVAL numcaps=$DEFAULT_NUMCAPS } # Do an override # It takes basically an assignment (in the same format as bash) # to one of the override-able variables (see $ALLOWED_OVERRIDES). # There are some restrictions though. Currently ';' is not allowed to # be in the assignment. # override($1 = bash variable assignment, $2 = source) override() { local o="$1" local src="$2" local compregex=$( sed 's/ /|/g' <<<${ALLOWED_OVERRIDES[*]} ) # Don't allow ';', FIXME: dunno how secure that really is... # FIXME: ...it doesn't really works anyway if ! egrep -q '^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*=[^;]*' <<<"$o" ; then return fi if ! egrep -q "^($compregex)=" <<<"$o" ; then return fi local varname=$(sed -r 's/^[[:space:]]*([a-zA-Z0-9_]*)=.*/\1/'<<<"$o") local varval=$(sed -r 's/[^=]*=(.*)/\1/'<<<"$o") # FIXME: Security! local curvarval= eval curvarval='$'"$varname" if [ "$curvarval" == "$varval" ]; then warn "Ignored override '$varname' (already had same value)" else eval "$varname=\"$varval\"" # FIXME: Only for really overridden ones warn "Overridden variable '$varname' from $src" fi } # }}} # Configuration handling # {{{ # Convenience functions # Returns true if input is composed only of numbers # is_number($1 = input) is_number() { egrep -q '^[0-9]+$' <<<"$1" } # Returns true if input can be parsed as a floating point number # Accepted: XX.YY XX. .YY (.24=0.24 # is_float($1 = input) is_float() { egrep -q '^([0-9]+\.?([0-9])?+|(\.[0-9]+))$'<<<"$1" } # Returns true if input is a fraction (*strictly*, i.e. "1" is not a fraction) # Only accepts XX/YY # is_fraction($1 = input) is_fraction() { egrep -q '^[0-9]+/[0-9]+$'<<<"$1" } # Makes a string lowercase # tolower($1 = string) tolower() { tr '[A-Z]' '[a-z]' <<<"$1" } # Rounded product # multiplies parameters and prints the result, rounded to the closest int # parameters can be separated by commas or spaces # e.g.: rmultiply 4/3,576 OR 4/3 576 = 4/3 * 576 = 768 # rmultiply($1 = operator1, [$2 = operator2, ...]) # rmultiply($1 = "operator1,operator2,...") rmultiply() { local exp=$(sed 's/[ ,]/*/g'<<<"$@") # bc expression #local f=$(bc -lq<<<"$exp") # exact float value # division is integer by default (without -l) so it's the smae # as rounding to the lower int #bc -q <<<"( $f + 0.5 ) / 1" bc -q <<<"scale=5; v=( ($exp) + 0.5 ) ; scale=0 ; v/1" } # Like rmultiply() but always rounded upwards ceilmultiply() { local exp=$(sed 's/[ ,]/*/g'<<<"$@") # bc expression local f=$(bc -lq<<<"$exp") # exact float value bc -q <<<"( $f + 0.999999999 ) / 1" } # Round to a multiple # Rounds a number ($1) to a multiple of ($2) # rtomult($1 = number, $2 = divisor) rtomult() { local n=$1 d=$2 local r=$(( $n % $d )) if [ $r -ne 0 ]; then let 'n += ( d - r )' fi echo $n } # numeric test eqivalent for floating point # fptest($1 = op1, $2 = operator, $3 = op2) fptest() { local op= case $2 in -gt) op='>' ;; -lt) op='<' ;; -ge) op='>=' ;; -le) op='<=' ;; -eq) op='==' ;; -ne) op='!=' ;; *) error "Internal error" && return $EX_SOFTWARE esac [ '1' == $(bc -q <<<"$1 $op $3") ] } # Applies the Pythagorean Theorem # pyth_th($1 = cathetus1, $2 = cathetus2) pyth_th() { bc -ql <<<"sqrt( $1^2 + $2^2)" } # Prints the width correspoding to the input height and the variable # aspect ratio # compute_width($1 = height) (=AR*height) (rounded) compute_width() { rmultiply $aspect_ratio,$1 } # Parse an interval and print the corresponding value in seconds # returns something not 0 if the interval is not recognized. # # The current code is a tad permissive, it allows e.g. things like # 10m1h (equivalent to 1h10m) # 1m1m (equivalent to 2m) # I don't see reason to make it more anal, though. # get_interval($1 = interval) get_interval() { if is_number "$1" ; then echo $1 ; return 0 ; fi local s=$(tolower "$1") t r # Only allowed characters if ! egrep -qi '^[0-9smh.]+$' <<<"$s"; then return $EX_USAGE; fi # New parsing code: replaces units by a product # and feeds the resulting string to bc t=$s t=$(sed -r 's/([0-9]+)h/ ( \1 * 3600 ) + /g' <<<$t) t=$(sed -r 's/([0-9]+)m/ ( \1 * 60 ) + /g' <<<$t) t=$(sed 's/s/ + /g' <<<$t) t=$(sed -r 's/\.\.+/./g'<<<$t) t=$(sed -r 's/(\.[0-9]+)/0\1 + /g' <<<$t) t=$(sed -r 's/\+ ?$//g' <<<$t) r=$(bc -lq <<<$t 2>/dev/null) # bc parsing fails with correct return code if [ -z "$r" ]; then return $EX_USAGE fi # Negative interval if [ "-" == ${r:0:1} ]; then return $EX_USAGE fi echo $r } # Pads a string with zeroes on the left until it is at least # the indicated length # pad($1 = minimum length, $2 = string) pad() { # printf "%0${1}d\n" "$2" # [[R1#18]] # Can't be used with non-numbers local str=$2 while [ "${#str}" -lt $1 ]; do str="0$str" done echo $str } # Get Image Width # imw($1 = file) imw() { identify "$1" | cut -d' ' -f3 | cut -d'x' -f1 } # Get Image Height # imh($1 = file) imh() { identify "$1" | cut -d' ' -f3 | cut -d'x' -f2 } # Prints a number of seconds in a more human readable form # e.g.: 3600 becomes 1:00:00 # pretty_stamp($1 = seconds) pretty_stamp() { if ! is_float "$1" ; then return $EX_USAGE ; fi local t=$1 #local h=$(( $t / 3600 )) # bc's modulus seems to *require* not using the math lib (-l) local h=$( bc -q <<<"$t / 3600") t=$(bc -q <<<"$t % 3600") local m=$( bc -q <<<"$t / 60") t=$(bc -q <<<"$t % 60") local s=$(cut -d'.' -f1 <<<$t) local ms=$(cut -d'.' -f2 <<<$t) local R="" if [ $h -gt 0 ]; then R="$h:" # Unreproducible bug reported by wdef: Minutes printed as hours # fixed with "else R="00:"" fi R="$R$(pad 2 "$m"):$(pad 2 $s)" # Milliseconds, only supported by ffmpeg, not printed otherwise if [ $decoder -eq $DEC_FFMPEG ]; then # Right pad of decimal seconds if [ ${#ms} -lt 2 ]; then ms="${ms}0" fi R="$R.$ms" fi # Trim (most) decimals sed -r 's/\.([0-9][0-9]).*/.\1/'<<<$R } # Prints the size of a file in a human friendly form # The units are in the IEC/IEEE/binary format (e.g. MiB -for mebibytes- # instead of MB -for megabytes-) # get_pretty_size($1 = file) get_pretty_size() { local f="$1" local bytes=$(get_file_size "$f") local size="" if [ "$bytes" -gt $(( 1024**3 )) ]; then local gibs=$(( $bytes / 1024**3 )) local mibs=$(( ( $bytes % 1024**3 ) / 1024**2 )) size="${gibs}.${mibs:0:2} GiB" elif [ "$bytes" -gt $(( 1024**2)) ]; then local mibs=$(( $bytes / 1024**2 )) local kibs=$(( ( $bytes % 1024**2 ) / 1024 )) size="${mibs}.${kibs:0:2} MiB" elif [ "$bytes" -gt 1024 ]; then local kibs=$(( $bytes / 1024 )) bytes=$(( $bytes % 1024 )) size="${kibs}.${bytes:0:2} KiB" else size="${bytes} B" fi echo $size } # Rename a file, if the target exists, try with appending numbers to the name # And print the output name to stdout # See $safe_rename_pattern # safe_rename($1 = original file, $2 = target file) # XXX: Note it fails if target has no extension safe_rename() { local from="$1" local to="$2" # Output extension local ext=$(sed -r 's/.*\.(.*)/\1/' <<<$to) # Input extension local iext=$(sed -r 's/.*\.(.*)/\1/' <<<$to) # Input filename without extension local b=${to%.$iext} # safe_rename_pattern is override-able, ensure it has a valid value: if ! grep -q '%e' <<<"$safe_rename_pattern" || ! grep -q '%N' <<<"$safe_rename_pattern" || ! grep -q '%b' <<<"$safe_rename_pattern" ; then safe_rename_pattern=$DEFAULT_SAFE_REN_PATT fi local n=1 while [ -f "$to" ]; do # Only executes if $2 exists to=$(sed "s#%b#$b#g" <<<"$safe_rename_pattern") to=$(sed "s#%N#$n#g" <<<"$to") to=$(sed "s#%e#$ext#g" <<<"$to") let 'n++'; done mv -- "$from" "$to" echo "$to" } # Gets the file size in bytes # get_file_size($1 = filename) # du can provide bytes or kilobytes depending on the version used. The difference # can be notorius... # At least the busybox implementation is a real world du in usage that doesn't allow # using --bytes. Note that using "ls -H" is not an option either for the same reason. get_file_size() { # First, try the extended du arguments: local bytes bytes=$(du -L --bytes "$1" 2>/dev/null) || { echo $(( 1024 * $(du -Lk "$1" | cut -f1) )) return } # Getting to here means the first du worked correctly cut -f1 <<<"$bytes" } # Tests the presence of all required programs # test_programs() test_programs() { local retval=0 last=0 for prog in getopt mplayer convert montage identify bc \ ffmpeg mktemp sed grep egrep cut; do type -pf "$prog" >/dev/null if [ $? -ne 0 ] ; then error "Required program $prog not found!" let 'retval++' fi done # TODO: [x2] return $retval } # Remove any temporal files # Does nothing if none has been created so far # cleanup() cleanup() { if [ -z $TEMPSTUFF ]; then return 0 ; fi inf "Cleaning up..." rm -rf ${TEMPSTUFF[*]} unset TEMPSTUFF ; declare -a TEMPSTUFF } # Exit callback. This function is executed on exit (correct, failed or # interrupted) # exithdlr() exithdlr() { cleanup } # Feedback handling, these functions are use to print messages respecting # the verbosity level # Optional color usage added from explanation found in # # # error($1 = text) error() { if [ $verbosity -ge $V_ERROR ]; then if [ $plain_messages -eq 0 ]; then tput bold ; tput setaf 1; fi # sgr0 is always used, this way if # a) something prints inbetween messages it isn't affected # b) if plain_messages is overridden colour stops after the override echo "$1" ; tput sgr0 fi >&2 # It is important to redirect both tput and echo to stderr. Otherwise # n=$(something) wouldn't be coloured } # # Print a non-fatal error or warning # warning($1 = text) warn() { if [ $verbosity -ge $V_WARN ]; then if [ $plain_messages -eq 0 ]; then tput bold ; tput setaf 3; fi echo "$1" ; tput sgr0 fi >&2 } # # Print an informational message # inf($1 = text) inf() { if [ $verbosity -ge $V_INFO ]; then if [ $plain_messages -eq 0 ]; then tput bold ; tput setaf 2; fi echo "$1" ; tput sgr0 fi >&2 } # # Same as inf but with no colour ever. # infplain($1 = text) infplain() { if [ $verbosity -ge $V_INFO ]; then echo "$1" >&2 fi } # # trace($1 = function name = $FUNCNAME, function arguments...) trace() { if [ "$DEBUG" -ne "1" ]; then return; fi echo "[TRACE]: $@" >&2 } # }}} # Convenience functions # {{{ # Core functionality # Creates a new temporary directory # create_temp_dir() create_temp_dir() { trace $FUNCNAME $@ # Try to use /dev/shm if available, this provided a very small # benefit on my system but me of help for huge files. Or maybe won't. if [ -d /dev/shm ] && [ -w /dev/shm ]; then VCSTEMPDIR=$(mktemp -d -p /dev/shm vcs.XXXXXX) else VCSTEMPDIR=$(mktemp -d -t vcs.XXXXXX) fi if [ ! -d "$VCSTEMPDIR" ]; then error "Error creating temporary directory" return $EX_CANTCREAT fi TEMPSTUFF+=( "$VCSTEMPDIR" ) } # Create a new temporal file and print its filename # new_temp_file($1 = suffix) new_temp_file() { trace $FUNCNAME $@ local r=$(mktemp -p "$VCSTEMPDIR" "vcs-XXXXXX") if [ ! -f "$r" ]; then error "Failed to create temporary file" return $EX_CANTCREAT fi r=$(safe_rename "$r" "$r$1") || { error "Failed to create temporary file" return $EX_CANTCREAT } TEMPSTUFF+=( "$r" ) echo "$r" } # Randomizes the colours and fonts. The result won't be of much use # in most cases but it might be a good way to discover some colour/font # or colour combination you like. # randomize_look() randomize_look() { trace $FUNCNAME $@ local mode=f lineno if [ "f" == $mode ]; then # Random mode # There're 5 rows of extra info printed local ncolours=$(( $(convert -list color | wc -l) - 5 )) randcolour() { lineno=$(( 5 + ( $RANDOM % $ncolours ) )) convert -list color | sed -n "${lineno}p" | cut -d' ' -f1 # [[R1#19]] } else # Pseudo-random mode, WIP! randccomp() { # colours are in the 0..65535 range, while RANDOM in 0..32767 echo $(( $RANDOM + $RANDOM + ($RANDOM % 1) )) } randcolour() { echo "rgb($(randccomp),$(randccomp),$(randccomp))" } fi local nfonts=$(( $(convert -list type | wc -l) - 5 )) randfont() { lineno=$(( 5 + ( $RANDOM % $nfonts ))) convert -list type | sed -n "${lineno}p" | cut -d' ' -f1 # [[R1#19]] } bg_heading=$(randcolour) bg_sign=$(randcolour) bg_title=$(randcolour) bg_contact=$(randcolour) fg_heading=$(randcolour) fg_sign=$(randcolour) fg_tstamps=$(randcolour) fg_title=$(randcolour) font_tstamps=$(randfont) font_heading=$(randfont) font_sign=$(randfont) font_title=$(randfont) inf "Randomization result: Chosen backgrounds: '$bg_heading' for the heading '$bg_sign' for the signature '$bg_title' for the title '$bg_contact' for the contact sheet Chosen font colours: '$fg_heading' for the heading '$fg_sign' for the signature '$fg_title' for the title '$fg_tstamps' for the timestamps, Chosen fonts: '$font_heading' for the heading '$font_sign' for the signature '$font_title' for the title '$font_tstamps' for the timestamps" unset -f randcolour randfound randccomp } # Add to $TIMECODES the timecodes at which a capture should be taken # from the current video # compute_timecodes($1 = timecode_from, $2 = interval, $3 = numcaps) compute_timecodes() { trace $FUNCNAME $@ local st=0 end=${VID[$LEN]} tcfrom=$1 tcint=$2 tcnumcaps=$3 eo=0 # globals: fromtime, totime, timecode_from, TIMECODES, end_offset if fptest $st -lt $fromtime ; then st=$fromtime fi if fptest $totime -gt 0 && fptest $end -gt $totime ; then end=$totime fi if fptest $totime -le 0 ; then # If no totime is set, use end_offset eo=$end_offset local runlen=$( bc <<<"$end - $st" ) if fptest "($end-$eo-$st)" -le 0 ; then if fptest "$eo" -gt 0 && fptest "$eo" -eq "$DEFAULT_END_OFFSET" ; then warn "Default end offset was too high, ignoring it." eo=0 else error "End offset too high, use e.g. '-E0'." return $EX_UNAVAILABLE fi fi fi local inc= if [ "$tcfrom" -eq $TC_INTERVAL ]; then inc=$tcint elif [ "$tcfrom" -eq $TC_NUMCAPS ]; then # Numcaps mandates: timecodes are obtained dividing the length # by the number of captures if [ $tcnumcaps -eq 1 ]; then # Special case, just one capture, center it inc=$( bc -lq <<< "scale=3; ($end-$st)/2 + 1" ) else #inc=$(( ($end-$st) / $tcnumcaps )) # FIXME: The last second is avoided (-1) to get the correct caps number inc=$( bc -lq <<< "scale=3; ($end-$eo-$st)/$tcnumcaps" ) fi else error "Internal error" return $EX_SOFTWARE fi if fptest $inc -gt ${VID[$LEN]}; then error "Interval is longer than video length, skipping $f" return $EX_USAGE fi local stamp=$st local -a LTC while fptest $stamp -le $(bc -q <<<"$end-$eo"); do if fptest $stamp -lt 0 ; then error "Internal error, negative timestamp calculated!" return $EX_SOFTWARE fi LTC+=( $stamp ) stamp=$(bc -q <<<"$stamp+$inc") done unset LTC[0] # Discard initial cap (=$st) TIMECODES=( ${TIMECODES[@]} ${LTC[@]} ) } # Tries to guess an aspect ratio comparing width and height to some # known values (e.g. VCD resolution turns into 4/3) # guess_aspect($1 = width, $2 = height) guess_aspect() { trace $FUNCNAME $@ # mplayer's ID_ASPECT seems to be always 0 ¿? local w=$1 h=$2 ar case "$w" in 352) if [ $h -eq 288 ] || [ $h -eq 240 ]; then # Ambiguous, could perfectly be 16/9 # VCD / DVD @ VCD Res. / Half-D1 / CVD ar=4/3 elif [ $h -eq 576 ] || [ $h -eq 480 ]; then # Ambiguous, could perfectly be 16/9 # Half-D1 / CVD ar=4/3 fi ;; 704|720) if [ $h -eq 576 ] || [ $h -eq 480 ]; then # DVD / DVB # Ambiguous, could perfectly be 16/9 ar=4/3 fi ;; 480) if [ $h -eq 576 ] || [ $h -eq 480 ]; then # SVCD ar=4/3 fi ;; esac if [ -z "$ar" ]; then if [ $h -eq 720 ] || [ $h -eq 1080 ]; then # HD # TODO: Is there a standard for PAL yet? ar=16/9 fi fi if [ -z "$ar" ]; then warn "Couldn't guess aspect ratio." ar="$w/$h" # Don't calculate it yet fi echo $ar } # Capture a frame # capture($1 = filename, $2 = second) capture() { trace $FUNCNAME $@ local f=$1 stamp=$2 local VIDCAPFILE=00000005.png # globals: $shoehorned $decoder if [ $decoder -eq $DEC_MPLAYER ]; then { # Capture 5 frames and drop the first 4, fixes a weird bug/feature of mplayer ([M1]) mplayer -sws 9 -ao null -benchmark -vo "png:z=0" -quiet \ -frames 5 -ss $stamp $shoehorned "$f" } >"$stdout" 2>"$stderr" rm -f 0000000{1,2,3,4}.png # Remove the first four elif [ $decoder -eq $DEC_FFMPEG ]; then # XXX: It would be nice to show a message if it takes too long { # See wa_ss_* declarations at the start of the file for details ffmpeg -y ${wa_ss_be/ / $stamp} -i "$f" ${wa_ss_af/ / $stamp} -an \ -dframes 1 -vframes 1 -vcodec png \ -f rawvideo $shoehorned $VIDCAPFILE # Used to test bogus files (e.g. to test codec ids) #convert -size 1x xc:black $VIDCAPFILE } >"$stdout" 2>"$stderr" else error "Internal error!" return $EX_SOFTWARE fi || { local retval=$? error "The capturing program failed!" return $retval } if [ ! -f "$VIDCAPFILE" ] || [ "0" == "$(du "$VIDCAPFILE" | cut -f1)" ]; then error "Failed to capture frame (at second $stamp)" return $EX_SOFTWARE fi return 0 } # Applies all individual vidcap filters # filter_vidcap($1 = filename, $2 = timestamp, $3 = width, $4 = height) filter_vidcap() { trace $FUNCNAME $@ # For performance purposes each filter simply prints a set of options # to 'convert'. That's less flexible but enough right now for the current # filters. local cmdopts= for filter in ${FILTERS_IND[@]}; do cmdopts="$cmdopts $( $filter "$1" "$2" "$3" "$4" ) " done local t=$(new_temp_file .png) eval "convert '$1' $cmdopts '$t'" # If $t doesn't exist returns non-zero [ -f "$t" ] && mv "$t" "$1" } # Applies all global vidcap filters #filter_all_vidcaps() { # # TODO: Do something with "$@" # true #} filt_resize() { trace $FUNCNAME $@ local f="$1" t=$2 w=$3 h=$4 # Note the '!', required to change the aspect ratio echo " \( -geometry ${w}x${h}! \) " } # Draw a timestamp in the file # filt_apply_stamp($1 = filename, $2 = timestamp, $3 = width, $4 = height) filt_apply_stamp() { trace $FUNCNAME $@ local filename=$1 timestamp=$2 width=$3 height=$4 echo -n " \( -box '#000000aa' -fill '$fg_tstamps' -pointsize '$pts_tstamps' " echo -n " -gravity '$grav_timestamp' -stroke none -strokewidth 3 -annotate +5+5 " echo " ' $(pretty_stamp $stamp) ' \) -flatten " } # Apply a Polaroid-like effect # Taken from # filt_photoframe($1 = filename, $2 = timestamp, $3 = width, $4 = height) filt_photoframe() { trace $FUNCNAME $@ # local file="$1" ts=$2 w=$3 h=$4 # Tweaking the size gives a nice effect too # w=$(( $w - ( $RANDOM % ( $w / 3 ) ) )) # TODO: Split softshadow in a filter echo -n "-bordercolor white -border 6 -bordercolor grey60 -border 1 " echo -n "-background black \( +clone -shadow 60x4+4+4 \) +swap " echo "-background none -flatten -trim +repage" } # Applies a random rotation # Taken from # filt_randrot($1 = filename, $2 = timestamp, $3 = width, $4 = height) filt_randrot() { trace $FUNCNAME $@ # Rotation angle [-18..18] local angle=$(( ($RANDOM % 37) - 18 )) echo "-background none -rotate $angle " } # This one requires much more work, the results are pretty rough, but ok as # a starting point / proof of concept filt_film() { trace $FUNCNAME $@ local file="$1" ts=$2 w=$3 h=$4 # Base reel dimensions local rw=$(rmultiply $w,0.08) # 8% width local rh=$(( $rw / 2 )) # Ellipse center local ecx=$(( $rw / 2 )) ecy=0 # Ellipse x, y radius local erx=$(( (rw/2)*60/100 )) # 60% halt rect width local ery=$(( $erx / 2)) local base_reel=$(new_temp_file .png) reel_strip=$(new_temp_file .png) # Create the reel pattern... convert -size ${rw}x${rh} 'xc:black' \ -fill white -draw "ellipse $ecx,$ecy $erx,$ery 0,360" -flatten \ \( +clone -flip \) -append \ -fuzz '40%' -transparent white \ "$base_reel" # FIXME: Error handling # Repeat it until the height is reached and crop to the exact height local sh=$(imh "$base_reel") in= local repeat=$( ceilmultiply $h/$sh) while [ $repeat -gt 1 ]; do in+=" '$base_reel' " let 'repeat--' done eval convert "$base_reel" $in -append -crop $(imw "$base_reel")x${h}+0+0 \ "$reel_strip" # As this options will be appended to the commandline we cannot # order the arguments optimally (eg: reel.png image.png reel.png +append) # A bit of trickery must be done flipping the image. Note also that the # second strip will be appended flipped, which is intended. echo -n "'$reel_strip' +append -flop '$reel_strip' +append -flop " } # Creates a contact sheet by calling the delegate # create_contact_sheet($1 = columns, $2 = context, $3 = width, $4 = height, # $5...$# = vidcaps) : output create_contact_sheet() { trace $FUNCNAME $@ $CSHEET_DELEGATE "$@" } # This is the standard contact sheet creator # csheet_montage($1 = columns, $2 = context, $3 = width, $4 = height, # $5... = vidcaps) : output csheet_montage() { trace $FUNCNAME $@ local cols=$1 ctx=$2 width=$3 height=$4 output=$(new_temp_file .png) shift 4 # Padding is no longer dependant upong context since alignment of the # captures was far trickier then local hpad=$HPAD vpad=4 # Using transparent seems to make -shadow futile montage -background Transparent "$@" -geometry +$hpad+$vpad -tile "$cols"x "$output" # This should actually be moved to a filter but with the current # architecture I'm unable to come up with the correct convert options if [ $DISABLE_SHADOWS -eq 0 ]; then # This produces soft-shadows, which look much better than the montage ones convert \( -shadow 50x2+10+10 "$output" \) "$output" -composite "$output" fi #convert \( -shadow 50x2+10+10 "$output" \) "$output" -composite "$output" # FIXME: Error handling echo $output } # Polaroid contact sheet creator: it overlaps vidcaps with some randomness # csheet_overlap($1 = columns, $2 = context, $3 = width, $4 = height, # $5... = $vidcaps) : output csheet_overlap() { trace $FUNCNAME $@ local cols=$1 ctx=$2 width=$3 height=$4 # globals: $VID shift 4 # TBD: Handle context # Explanation of how this works: # On the first loop we do what the "montage" command would do (arrange the # images in a grid) but overlapping each image to the one on their left, # creating the output row by row, each row in a file. # On the second loop we append the rows, again overlapping each one to the # one before (above) it. # XXX: Compositing over huge images is quite slow, there's probably a # better way to do it # Offset bounds, this controls how much of each snap will be over the # previous one. Note it is important to work over $width and not $VID[$W] # to cover all possibilities (extended mode and -H change the vidcap size) local maxoffset=$(( $width / 3 )) local minoffset=$(( $width / 6 )) # Holds the files that will form the full contact sheet # each file is a row on the final composition local -a rowfiles # Dimensions of the canvas for each row, it should be big enough # to hold all snaps. # My trigonometry is pretty rusty but considering we restrict the angle a lot # I believe no image should ever be wider/taller than the diagonal (note the # ceilmultiply is there to simply round the result) local diagonal=$(ceilmultiply $(pyth_th $width $height) 1) # XXX: The width, though isn't guaranteed (e.g. using filt_film it ends wider) # adding 3% to the diagonal *should* be enough to compensate local canvasw=$(( ( $diagonal + $(rmultiply $diagonal,0.3) ) * $cols )) local canvash=$(( $diagonal )) # The number of rows required to hold all the snaps local numrows=$(ceilmultiply ${#@},1/$cols) # rounded division # Variables inside the loop local col # Current column local rowfile # Holds the row we're working on local offset # Random offset of the current snap [$minoffset..$maxoffset] local accoffset # The absolute (horizontal) offset used on the next iteration local cmdopts # Holds the arguments passed to convert to compose the sheet local w # Width of the current snap for row in $(seq 1 $numrows) ; do col=0 rowfile=$(new_temp_file .png) rowfiles+=( "$rowfile" ) accoffset=0 cmdopts= # This command is pretty time-consuming, let's make it in a row # Base canvas inf "Creating polaroid base canvas $row/$numrows..." convert -size ${canvasw}x${canvash} xc:transparent "$rowfile" # Step through vidcaps (col=[0..cols-1]) for col in $(seq 0 $(( $cols - 1 ))); do # More cols than files in the last iteration (e.g. -n10 -c4) if [ -z "$1" ]; then break; fi w=$(imw "$1") # Stick the vicap in the canvas #convert -geometry +${accoffset}+0 "$rowfile" "$1" -composite "$rowfile" cmdopts="$cmdopts -geometry +${accoffset}+0 '$1' -composite " offset=$(( $minoffset + ( $RANDOM % $maxoffset ) )) let 'accoffset=accoffset + w - offset' shift done inf "Composing polaroid row $row/$numrows..." eval convert "'$rowfile'" "$cmdopts" -trim +repage "'$rowfile'" >&2 done inf "Merging polaroid rows..." output=$(new_temp_file .png) # Standard composition #convert -background Transparent "${rowfiles[@]}" -append polaroid.png # Overlapped composition convert -size ${canvasw}x$(( $canvash * $cols )) xc:transparent "$output" cmdopts= accoffset=0 local h for row in "${rowfiles[@]}" ; do w=$(imw "$row") h=$(imh "$row") minoffset=$(( $h / 8 )) maxoffset=$(( $h / 4 )) offset=$(( $minoffset + ( $RANDOM % $maxoffset ) )) # The row is also offset horizontally cmdopts="$cmdopts -geometry +$(( $RANDOM % $maxoffset ))+$accoffset '$row' -composite " let 'accoffset=accoffset + h - offset' done # After the trim the top corners are too near the heading, we add some space # with -splce eval convert -background transparent "$output" $cmdopts -trim +repage \ -bordercolor Transparent -splice 0x10 "$output" >&2 # FIXME: Error handling echo $output } # Sorts timestamps and removes duplicates # clean_timestamps($1 = space separated timestamps) clean_timestamps() { trace $FUNCNAME $@ # Note AFAIK sort only sorts lines, that's why y replace spaces by newlines local s=$1 sed 's/ /\n/g'<<<"$s" | sort -n | uniq } # Fills the $MPLAYER_CACHE and $VID variables with the video data # identify_video($1 = file) identify_video() { trace $FUNCNAME $@ local f=$1 # Meta data extraction # Note to self: Don't change the -vc as it would affect $vdec MPLAYER_CACHE=$(mplayer -benchmark -ao null -vo null -identify -frames 0 -quiet "$f" 2>/dev/null | grep ^ID) VID[$VCODEC]=$(grep ID_VIDEO_FORMAT <<<"$MPLAYER_CACHE" | cut -d'=' -f2) # FourCC VID[$ACODEC]=$(grep ID_AUDIO_FORMAT <<<"$MPLAYER_CACHE" | cut -d'=' -f2) VID[$VDEC]=$(grep ID_VIDEO_CODEC <<<"$MPLAYER_CACHE" | cut -d'=' -f2) # Decoder (!= Codec) VID[$W]=$(grep ID_VIDEO_WIDTH <<<"$MPLAYER_CACHE" | cut -d'=' -f2) VID[$H]=$(grep ID_VIDEO_HEIGHT <<<"$MPLAYER_CACHE" | cut -d'=' -f2) VID[$FPS]=$(grep ID_VIDEO_FPS <<<"$MPLAYER_CACHE" | cut -d'=' -f2) VID[$LEN]=$(grep ID_LENGTH <<<"$MPLAYER_CACHE"| cut -d'=' -f2) # For some reason my (one track) samples have two ..._NCH, first one 0 VID[$CHANS]=$(grep ID_AUDIO_NCH <<<"$MPLAYER_CACHE"|cut -d'=' -f2|head -2|tail -1) # Upon consideration: #if grep -q '\.[0-9]*0$' <<<${VID[$FPS]} ; then # # Remove trailing zeroes... # VID[$FPS]=$(sed -r 's/(\.[1-9]*)0*$/\1/' <<<${VID[$FPS]}) # # ...And trailing decimal point # VID[$FPS]=$(sed 's/\.$//'<<<${VID[$FPS]}) #fi # Voodoo :P Remove (one) trailing zero if [ "${VID[$FPS]:$(( ${#VID[$FPS]} - 1 ))}" == "0" ]; then VID[$FPS]="${VID[$FPS]:0:$(( ${#VID[$FPS]} - 1 ))}" fi # Check sanity of the most important values is_number "${VID[$W]}" && is_number "${VID[$H]}" && is_float "${VID[$LEN]}" } # Main function. # Creates the contact sheet. # process($1 = file) process() { trace $FUNCNAME $@ local f=$1 local numcols= if [ ! -f "$f" ]; then error "File \"$f\" doesn't exist" return $EX_NOINPUT fi inf "Processing $f..." identify_video "$f" || { error "Found unsupported value while identifying video. Can't continue." return $EX_SOFTWARE } # Vidcap/Thumbnail height local vidcap_height=$th_height if ! is_number "$vidcap_height" || [ "$vidcap_height" -eq 0 ]; then vidcap_height=${VID[$H]} fi if [ "0" == "$aspect_ratio" ]; then aspect_ratio=$(bc -lq <<< "${VID[$W]} / ${VID[$H]}") elif [ "-1" == "$aspect_ratio" ]; then aspect_ratio=$(guess_aspect ${VID[$W]} ${VID[$H]}) inf "Aspect ratio set to $(sed -r 's/(\.[0-9]{2}).*/\1/g'<<<$aspect_ratio)" fi local vidcap_width=$(compute_width $vidcap_height) local numsecs=$(grep ID_LENGTH <<<"$MPLAYER_CACHE"| cut -d'=' -f2 | cut -d. -f1) local nc=$numcaps create_temp_dir unset TIMECODES # Compute the stamps (if in auto mode)... TIMECODES=${initial_stamps[*]} if [ $manual_mode -ne 1 ]; then compute_timecodes $timecode_from $interval $numcaps || { return $? } fi local base_montage_command="montage -font $font_tstamps -pointsize $pts_tstamps \ -gravity SouthEast -fill white " local output=$(new_temp_file '-preview.png') local VIDCAPFILE=00000005.png # If the temporal vidcap already exists, abort if [ -f $VIDCAPFILE ]; then error "File 0000000$f.png exists and would be overwritten, move it out before running." return $EX_CANTCREAT fi # mplayer will re-write also 00000001.png-00000004.png if [ $decoder -eq $DEC_MPLAYER ]; then for f_ in 1 2 3 4; do if [ -f "0000000${f_}.png" ]; then error "File 0000000${f_}.png exists and would be overwritten, move it out before running." return $EX_CANTCREAT fi done fi TEMPSTUFF+=( $VIDCAPFILE ) # Highlights local hlfile n=1 # hlfile Must be outside the if! if [ "$HLTIMECODES" ]; then local hlcapfile= pretty= local -a capfiles for stamp in $(clean_timestamps "${HLTIMECODES[*]}"); do if fptest $stamp -gt $numsecs ; then let 'n++' && continue ; fi pretty=$(pretty_stamp $stamp) inf "Generating highlight #${n}/${#HLTIMECODES[*]} ($pretty)..." capture "$f" $stamp || return $? filter_vidcap "$VIDCAPFILE" $pretty $vidcap_width $vidcap_height || { local r=$? error "Failed to apply transformations to the capture." return $r } hlcapfile=$(new_temp_file "-hl-$(pad 6 $n).png") mv "$VIDCAPFILE" "$hlcapfile" capfiles+=( "$hlcapfile" ) let 'n++' done let 'n--' # There's an extra inc if [ "$n" -lt "$cols" ]; then numcols=$n else numcols=$cols fi inf "Composing highlights contact sheet..." hlfile=$( create_contact_sheet $numcols $CTX_HL $vidcap_width $vidcap_height "${capfiles[@]}" ) unset hlcapfile pretty n capfiles numcols fi unset n # Normal captures # TODO: Don't reference $VIDCAPFILE local capfile pretty n=1 unset capfiles ; local -a capfiles for stamp in $(clean_timestamps "${TIMECODES[*]}"); do pretty=$(pretty_stamp $stamp) inf "Generating capture #${n}/${#TIMECODES[*]} ($pretty)..." capture "$f" $stamp || return $? filter_vidcap "$VIDCAPFILE" $pretty $vidcap_width $vidcap_height || return $? # identified by capture number, padded to 6 characters capfile=$(new_temp_file "-cap-$(pad 6 $n).png") mv "$VIDCAPFILE" "$capfile" capfiles+=( "$capfile" ) let 'n++' # $n++ done #filter_all_vidcaps "${capfiles[@]}" let 'n--' # there's an extra inc if [ "$n" -lt "$cols" ]; then numcols=$n else numcols=$cols fi inf "Composing standard contact sheet..." output=$(create_contact_sheet $numcols $CTX_STD $vidcap_width $vidcap_height "${capfiles[@]}") unset capfile capfiles pretty n # must carry on to the extended caps: numcols # Extended mode local extoutput= if [ "$extended_factor" != 0 ]; then # Number of captures. Always rounded to a multiplier of *double* the # number of columns (the extended caps are half width, this way they # match approx with the standard caps width) local hlnc=$(rtomult "$(( ${#TIMECODES[@]} * $extended_factor ))" $((2*$numcols))) unset TIMECODES # required step to get the right count declare -a TIMECODES # Note the manual stamps are not included anymore compute_timecodes $TC_NUMCAPS "" $hlnc unset hlnc local n=1 w= h= capfile= pretty= unset capfiles ; local -a capfiles # The image size of the extra captures is 1/4, adjusted to compensante the padding let 'w=vidcap_width/2-HPAD, h=vidcap_height*w/vidcap_width' for stamp in $(clean_timestamps "${TIMECODES[*]}"); do pretty=$(pretty_stamp $stamp) inf "Generating capture from extended set: ${n}/${#TIMECODES[*]} ($pretty)..." capture "$f" $stamp || return $? filter_vidcap "$VIDCAPFILE" $pretty $w $h || return $? capfile=$(new_temp_file "-excap-$(pad 6 $n).png") mv "$VIDCAPFILE" "$capfile" capfiles+=( "$capfile" ) let 'n++' done let 'n--' # There's an extra inc if [ $n -lt $(( $cols * 2 )) ]; then numcols=$n else numcols=$(( $cols * 2 )) fi inf "Composing extended contact sheet..." extoutput=$( create_contact_sheet $numcols $CTX_EXT $w $h "${capfiles[@]}" ) unset w h capfile pretty n numcols fi # Extended mode # Video codec "prettyfication", see [[R2]], [[R3]], [[R4]] local vcodec= acodec= case "${VID[$VCODEC]}" in 0x10000001) vcodec="MPEG-1" ;; 0x10000002) vcodec="MPEG-2" ;; 0x00000000) vcodec="Raw RGB" ;; # How correct is this? avc1) vcodec="MPEG-4 AVC" ;; DIV3) vcodec="DivX ;-) Low-Motion" ;; # Technically same as mp43 DX50) vcodec="DivX 5" ;; FMP4) vcodec="FFmpeg" ;; # XXX: Would LAVC be a better name? I420) vcodec="Raw I420 Video" ;; # XXX: Officially I420 is Indeo 4 but it is mapped to raw ¿? MJPG) vcodec="M-JPEG" ;; # XXX: Actually mJPG != MJPG MPG4) vcodec="MS MPEG-4 V1" ;; MP42) vcodec="MS MPEG-4 V2" ;; MP43) vcodec="MS MPEG-4 V3" ;; RV10) vcodec="RealVideo 1.0/5.0" ;; RV20) vcodec="RealVideo G2" ;; RV30) vcodec="RealVideo 8" ;; RV40) vcodec="RealVideo 9/10" ;; SVQ1) vcodec="Sorenson Video 1" ;; SVQ3) vcodec="Sorenson Video 3" ;; theo) vcodec="Ogg Theora" ;; tscc) vcodec="TechSmith Screen Capture Codec" ;; VP6[012]) vcodec="On2 Truemotion VP6" ;; WMV1) vcodec="WMV7" ;; WMV2) vcodec="WMV8" ;; WMV3) vcodec="WMV9" ;; WMVA) vcodec="WMV9 Advanced Profile" ;; # Not VC1 compliant. Deprecated by Microsoft. XVID) vcodec="Xvid" ;; # These are known FourCCs that I haven't tested against so far WVC1) vcodec="VC-1" ;; DIV4) vcodec="DivX ;-) Fast-Motion" ;; DIVX|divx) vcodec="DivX" ;; # OpenDivX / DivX 5(?) / Project Mayo IV4[0-9]) vcodec="Indeo Video 4" ;; IV50) vcodec="Indeo 5.0" ;; VP3[01]) vcodec="On2 VP3" ;; VP40) vcodec="On2 VP4" ;; VP50) vcodec="On2 VP5" ;; # Legacy(-er) codecs (haven't seen files in these formats in awhile) IV3[0-9]) vcodec="Indeo Video 3" ;; MSVC) vcodec="Microsoft Video 1" ;; MRLE) vcodec="Microsoft RLE" ;; *) # If not recognized show FOURCC vcodec=${VID[$VCODEC]} ;; esac if [ "${VID[$VDEC]}" == "ffodivx" ]; then vcodec+=" (MPEG-4)" elif [ "${VID[$VDEC]}" == "ffh264" ]; then vcodec+=" (h.264)" fi # Audio codec "prettyfication", see [[R4]] case $(tolower ${VID[$ACODEC]} ) in 85) acodec='MPEG Layer III (MP3)' ;; 80) acodec='MPEG Layer I/II (MP1/MP2)' ;; # Apparently they use the same tag mp4a) acodec='MPEG-4 AAC' ;; # LC and HE, apparently 352) acodec='WMA7' ;; # =WMA1 353) acodec='WMA8' ;; # =WMA2 No idea if lossless can be detected 354) acodec='WMA9' ;; # =WMA3 8192) acodec='AC3' ;; 1|65534) # 1 is standard PCM (apparently all sample sizes) # 65534 seems to be multichannel PCM acodec='Linear PCM' ;; vrbs|22127) # 22127 = Vorbis in AVI (with ffmpeg) DON'T! # vrbs = Vorbis in Matroska, probably other sane containers acodec='Vorbis' ;; qdm2) acodec="QDesign" ;; "") acodec="no audio" ;; # Following not seen by me so far, don't even know if mplayer would # identify them # 355) acodec="WMA9 Lossless" ;; 10) acodec="WMA9 Voice" ;; *) # If not recognized show audio id tag acodec=${VID[$ACODEC]} ;; esac if [ "${VID[$CHANS]}" ] && is_number "${VID[$CHANS]}" &&[ ${VID[$CHANS]} -ne 2 ]; then if [ ${VID[$CHANS]} -eq 0 ]; then # This happens e.g. in non-i386 when playing WMA9 at the time of # this writing warn "Detected 0 audio channels." warn " Does this version of mplayer support the audio codec ($acodec)?" elif [ ${VID[$CHANS]} -eq 1 ]; then acodec+=" (mono)" else acodec+=" (${VID[$CHANS]}ch)" fi fi if [ "$HLTIMECODES" ] || [ "$extended_factor" != "0" ]; then inf "Merging contact sheets..." fi # If there were highlights then mix them in if [ "$HLTIMECODES" ]; then #\( -geometry x2 xc:black -background black \) # This breaks it! convert \( -background LightGoldenRod "$hlfile" -flatten \) \ \( "$output" \) -append "$output" fi # Extended captures if [ "$extended_factor" != 0 ]; then convert "$output" "$extoutput" -append "$output" fi # Add the background convert -background "$bg_contact" "$output" -flatten "$output" # Let's add meta inf and signature inf "Adding header and footer..." local meta2="Dimensions: ${VID[$W]}x${VID[$H]} Format: $vcodec / $acodec FPS: ${VID[$FPS]}" local signature if [ $anonymous_mode -eq 0 ]; then signature="$user_signature $user with $PROGRAM_SIGNATURE" else signature="Created with $PROGRAM_SIGNATURE" fi local headwidth=$(identify "$output" | cut -d' ' -f3 | cut -d'x' -f1) # TODO: Use a better height calculation local headheight=$(($pts_meta * 4 )) local heading=$(new_temp_file .png) # Add the title if any if [ "$title" ]; then # TODO: Use a better height calculation convert \ \( \ -size ${headwidth}x$(( $pts_title + ($pts_title/2) )) "xc:$bg_title" \ -font "$font_title" -pointsize "$pts_title" \ -background "$bg_title" -fill "$fg_title" \ -gravity Center -annotate 0 "$title" \ \) \ -flatten \ "$output" -append "$output" fi local fn_font= # see $font_filename case $font_filename in $FF_DEFAULT) fn_font="$font_heading" ;; $FF_MINCHO) fn_font="$FONT_MINCHO" ;; *) warn "\$font_filename was overridden with an incorrect value, using default." fn_font="$font_heading" ;; esac # Talk about voodoo... feel the power of IM... let's try to explain what's this: # It might technically be wrong but it seems to work as I think it should # (hence the voodoo I was talking) # Parentheses restrict options inside them to only affect what's inside too # * Create a base canvas of the desired width and height 1. The width is tweaked # because using "label:" later makes the text too close to the border, that # will be compensated in the last step. # * Create independent intermediate images with each row of information, the # filename row is split in two images to allow changing the font, and then # they're horizontally appended (and the font reset) # * All rows are vertically appended and cropped to regain the width in case # the filename is too long # * The appended rows are appended to the original canvas, the resulting image # contains the left row of information with the full heading width and # height, and this is the *new base canvas* # * Draw over the new canvas the right row with annotate in one # operation, the offset compensates for the extra pixel from the original # base canvas. XXX: Using -annotate allows setting alignment but it breaks # vertical alignment with the other rows' labels. # * Finally add the border that was missing from the initial width, we have # now the *complete header* # * Add the contact sheet and append it to what we had. # * Start a new image and annotate it with the signature, then append it too. convert \ \( \ -size $(( ${headwidth} -18 ))x1 "xc:$bg_heading" +size \ -font "$font_heading" -pointsize "$pts_meta" \ -background "$bg_heading" -fill "$fg_heading" \ \( \ -gravity West \ \( label:"Filename:" \ -font "$fn_font" label:"$(basename "$f")" +append \ \) \ -font "$font_heading" \ label:"File size: $(get_pretty_size "$f")" \ label:"Length: $(cut -d'.' -f1 <<<$(pretty_stamp ${VID[$LEN]}))" \ -append -crop ${headwidth}x${headheight}+0+0 \ \) \ -append \ \( \ -size ${headwidth}x${headheight} \ -gravity East -fill "$fg_heading" -annotate +0-1 "$meta2" \ \) \ -bordercolor "$bg_heading" -border 9 \ \) \ "$output" -append \ \( \ -size ${headwidth}x34 -gravity Center "xc:$bg_sign" \ -font "$font_sign" -pointsize "$pts_sign" \ -fill "$fg_sign" -annotate 0 "$signature" \ \) \ -append \ "$output" unset signature meta2 headwidth headheight heading fn_font if [ $output_format != "png" ]; then local newout="$(dirname "$output")/$(basename "$output" .png).$output_format" convert -quality $output_quality "$output" "$newout" output="$newout" fi output_name=$( safe_rename "$output" "$(basename "$f").$output_format" ) || { error "Failed to write the output file!" return $EX_CANTCREAT } inf "Done. Output wrote to $output_name" cleanup } # }}} # Core functionality # {{{ # Debugging helpers # Tests integrity of some operations. # Used to test internal changes for consistency. # It helps me to identify incorrect optimizations. # unit_test() unit_test() { local t op val ret comm retval=0 # Textual tests, compare output to expected output # Tests are in the form "operation arguments correct_result #Description" local TESTS=( "rmultiply 1,1 1 #Identity" "rmultiply 16/9,1 2 #Rounding" # 1.77 rounded 2 "rmultiply 4/3 1 #Rounding" # 1.33 rounded 1 "rmultiply 1,16/9 2 #Commutative property" "rmultiply 1.7 2 #Alternate syntax" "ceilmultiply 1,1 1 #" "ceilmultiply 4/3 2 #" # 1.33 rounded 2 "ceilmultiply 7,1/2 4 #" # 3.5 rounded 4 "ceilmultiply 7/2 4 #Alternative syntax" "ceilmultiply 1/2,7 4 #Commutative property" "pad 10 0 0000000000 #Padding" "pad 1 20 20 #Unneeded padding" "pad 5 23.3 023.3 #Floating point padding" "guess_aspect 720 576 4/3 #DVD AR Guess" "guess_aspect 1024 576 1024/576 #Unsupported Wide AR Guess" "tolower ABC abc #lowercase conversion" "pyth_th 4 3 5 #Integer pythagorean theorem" "pyth_th 16 9 18.35755975068581929849 #FP pythagorean theorem" ) for t in "${TESTS[@]}" ; do # Note the use of ! as separator, this is because # and / are used in # many of the inputs comm=$(sed 's!.* #!!g' <<<$t) # Expected value val=$(sed -r "s!.* (.*) #$comm\$!\1!g"<<<$t) op=$(sed "s! $val #$comm\$!!g" <<<$t) if [ -z "$comm" ]; then comm=unnamed fi ret=$($op) || true if [ "$ret" != "$val" ] && fptest "$ret" -ne "$val" ; then error "Failed test ($comm): '$op $val'. Got result '$ret'." let 'retval++,1' # The ,1 ensures let doesn't fail else inf "Passed test ($comm): '$op $val'." fi done # Returned value tests, compare return to expected return local TESTS=( # Don't use anything with a RE meaning # Floating point numeric "test" "fptest 3 -eq 3 0 #FP test" "fptest 3.2 -gt 1 0 #FP test" "fptest 1/2 -le 2/3 0 #FP test" "fptest 6.34 -gt 6.34 1 #FP test" "fptest 1>0 -eq 1 0 #FP -logical- test" "is_number 3 0 #Numeric recognition" "is_number '3' 1 #Quoted numeric recognition" "is_number 3.3 1 #Non-numeric recognition" "is_float 3.33 0 #Float recognition" "is_float 3 0 #Float recognition" "is_float 1/3 1 #Non-float recognition" "is_fraction 1/1 0 #Fraction recognition" "is_fraction 1 1 #non-fraction recognition" "is_fraction 1.1 1 #Non-fraction recognition" ) for t in "${TESTS[@]}"; do comm=$(sed 's!.* #!!g' <<<$t) # Expected value val=$(sed -r "s!.* (.*) #$comm\$!\1!g"<<<$t) op=$(sed "s! $val #$comm\$!!g" <<<$t) if [ -z "$comm" ]; then comm=unnamed fi ret=0 $op || { ret=$? } if [ $val -eq $ret ]; then inf "Passed test ($comm): '$op; returns $val'." else error "Failed test ($comm): '$op; returns $val'. Returned '$ret'" let 'retval++,1' fi done return $retval } # }}} # Debugging helpers # {{{ # Help / Info # Prints the program identification to stderr show_vcs_info() { # Won't be printed in quiet modes inf "Video Contact Sheet *NIX v${VERSION}, (c) 2007 Toni Corvera" "sgr0" } # Prints the list of options to stdout show_help() { local P=$(basename $0) cat < Options: -i|--interval Set the interval to arg. Units can be used (case-insensitive), i.e.: Seconds: 90 or 90s Minutes: 3m Hours: 1h Combined: 1h3m90 Use either -i or -n. -n|--numcaps Set the number of captured images to arg. Use either -i or -n. -c|--columns Arrange the output in 'arg' columns. -H|--height Set the output (individual thumbnail) height. Width is derived accordingly. Note width cannot be manually set. -a|--aspect Aspect ratio. Accepts a floating point number or a fraction. -f|--from Set starting time. No caps before this. Same format as -i. -t|--to Set ending time. No caps beyond this. Same format as -i. -E|--end_offset This time is ignored, from the end of the video. Same format as -i. This value is not used when a explicit ending time is set. By default it is $DEFAULT_END_OFFSET. -T|--title Add a title above the vidcaps. -j|--jpeg Output in jpeg (by default output is in png). -q|--quiet Don't print progess messages just errors. Repeat to mute completely even on error. -h|--help Show this text. -Wo Workaround: Change ffmpeg's arguments order, might work with some files that fail otherwise. -d|--disable Disable some default functionality. Features that can be disabled are: * timestamps: use -dt or --disable timestamps * shadows: use -ds or --disable shadows -A|--autoaspect Try to guess aspect ratio from resolution. -e[num] | --extended=[num] Enables extended mode and optionally sets the extended factor. -e is the same as -e$DEFAULT_EXT_FACTOR. -l|--highlight Add the image found at the timestamp "arg" as a highlight. Same format as -i. -m|--manual Manual mode: Only timestamps indicated by the user are used (use in conjunction with -S), when using this -i and -n are ignored. -O|--override Use it to override a variable (see the homepage for more details). Format accepted is 'variable=value' (can also be quoted -variable="some value"- and can take an internal variable too -variable="\$SOME_VAR"-). -S|--stamp Add the image found at the timestamp "arg". Same format as -i. -u|--user Set the username found in the signature to this. -U|--fullname Use user's full/real name (e.g. John Smith) as found in /etc/passwd. -Ij|-Ik --mincho Use the kana/kanji/hiragana font (EXPERIMENTAL) might also work partially with Hangul and Cyrillic. -k --funky Funky modes: These are toy output modes in which the contact sheet gets a more informal look. Order *IS IMPORTANT*. A bad order gets a bad result :P They're random in nature so using the same funky mode twice will usually lead to quite different results. Currently available "funky modes": "overlap": Use '-ko' or '--funky overlap' Randomly overlap captures. "rotate": Use '-kr' or '--funky rotate' Randomly rotate each image. "photoframe": Use '-kf' or '--funky photoframe' Adds a photo-like white frame to each image. "polaroid": Use '-kp' or '--funky polaroid' Combination of rotate, photoframe and overlap. Same as -kr -ko -kf. "film": Use '-ki' or '--funky film' Imitates filmstrip look. "random": Use '-kx' or '--funky random' Randomizes colours and fonts. Options used for debugging purposes or to tweak the internal workings: -M|--mplayer Force the usage of mplayer. -F|--ffmpeg Force the usage of ffmpeg. --shoehorn Pass "arg" to mplayer/ffmpeg. You shouldn't need it. -D Debug mode. Used to test features/integrity. It: * Prints the input command line * Sets the title to reflect the command line * Does a basic test of consistency. Examples: Create a contact sheet with default values (vidcaps at intervals of $DEFAULT_INTERVAL seconds), the resulting file will be called input.avi.png: \$ $P input.avi Create a sheet with vidcaps at intervals of 3 and a half minutes: \$ $P -i 3m30 input.avi Create a sheet with vidcaps starting at 3 mins and ending at 18 mins, add an extra vidcap at 2m and another one at 19m: \$ $P -f 3m -t 18m -S2m -S 19m input.avi See more examples at vcs' homepage . EOF } # }}} # Help / Info #### Execution starts here #### # If tput isn't found simply ignore tput commands # (no colour support) # Important to do it before any message can be thrown if ! type -pf tput >/dev/null ; then tput() { cat >/dev/null <<<"$1"; } warn "tput wasn't found. Coloured feedback disabled." fi # Execute exithdlr on exit trap exithdlr EXIT show_vcs_info load_config # {{{ # Command line parsing # TODO: Find how to do this correctly (this way the quoting of $@ gets messed): #eval set -- "${default_options} ${@}" ARGS="$@" # [[R0]] TEMP=$(getopt -s bash -o i:n:u:T:f:t:S:jhFMH:c:ma:l:De::U::qAO:I::k:W:E:d: \ --long "interval:,numcaps:,username:,title:,from:,to:,stamp:,jpeg,help,"\ "shoehorn:,mplayer,ffmpeg,height:,columns:,manual,aspect:,highlight:,"\ "extended::,fullname,anonymous,quiet,autoaspect,override:,mincho,funky:,"\ "end_offset:,disable:" \ -n $0 -- "$@") eval set -- "$TEMP" while true ; do case "$1" in -i|--interval) if ! interval=$(get_interval "$2") ; then error "Incorrect interval format. Got '$2'." exit $EX_USAGE fi if [ "$interval" == "0" ]; then error "Interval must be higher than 0, set to the default $DEFAULT_INTERVAL" interval=$DEFAULT_INTERVAL fi timecode_from=$TC_INTERVAL shift # Option arg ;; -n|--numcaps) if ! is_number "$2" ; then error "Number of captures must be (positive) a number! Got '$2'." exit $EX_USAGE fi if [ $2 -eq 0 ]; then error "Number of captures must be greater than 0! Got '$2'." exit $EX_USAGE fi numcaps="$2" timecode_from=$TC_NUMCAPS shift # Option arg ;; -u|--username) user="$2" ; shift ;; -U|--fullname) # -U accepts an optiona argument, 0, to make an anonymous signature # --fullname accepts no argument if [ "$2" ]; then # With argument, special handling if [ "$2" != "0" ]; then error "Use '-U0' to make an anonymous contact sheet or '-u \"My Name\"'" error " to sign as My Name. Got -U$2" exit $EX_USAGE fi anonymous_mode=1 shift else # No argument, default handling (try to guess real name) user=$(grep ^$(id -un): /etc/passwd | cut -d':' -f5 |sed 's/,.*//g') if [ -z "$user" ]; then user=$(id -un) error "No fullname found, falling back to default ($user)" fi fi ;; --anonymous) anonymous_mode=1 ;; # Same as -U0 -T|--title) title="$2" ; shift ;; -f|--from) if ! fromtime=$(get_interval "$2") ; then error "Starting timestamp must be a valid timecode. Got '$2'." exit $EX_USAGE fi shift ;; -E|--end_offset) if ! end_offset=$(get_interval "$2") ; then error "End offset must be a valid timecode. Got '$2'." exit $EX_USAGE fi shift ;; -t|--to) if ! totime=$(get_interval "$2") ; then error "Ending timestamp must be a valid timecode. Got '$2'." exit $EX_USAGE fi if [ "$totime" -eq 0 ]; then error "Ending timestamp was set to 0, set to movie length." totime=-1 fi shift ;; -S|--stamp) if ! temp=$(get_interval "$2") ; then error "Timestamps must be a valid timecode. Got '$2'." exit $EX_USAGE fi initial_stamps=( ${initial_stamps[*]} $temp ) shift ;; -l|--highlight) if ! temp=$(get_interval "$2"); then error "Timestamps must be a valid timecode. Got '$2'." exit $EX_USAGE fi HLTIMECODES+=( $temp ) shift ;; -j|--jpeg) output_format=jpg ;; -h|--help) show_help ; exit $EX_OK ;; --shoehorn) shoehorned="$2" shift ;; -F) decoder=$DEC_FFMPEG ;; -M) decoder=$DEC_MPLAYER ;; -H|--height) if ! is_number "$2" ; then error "Height must be a (positive) number. Got '$2'." exit $EX_USAGE fi th_height="$2" shift ;; -a|--aspect) if ! is_float "$2" && ! is_fraction "$2" ; then error "Aspect ratio must be expressed as a (positive) floating " error " point number or a fraction (ie: 1, 1.33, 4/3, 2.5). Got '$2'." exit $EX_USAGE fi aspect_ratio="$2" shift ;; -A|--autoaspect) aspect_ratio=-1 ;; -c|--columns) if ! is_number "$2" ; then error "Columns must be a (positive) number. Got '$2'." exit $EX_USAGE fi cols="$2" shift ;; -m|--manual) manual_mode=1 ;; -e|--extended) # Optional argument quirks: $2 is always present, set to '' if unused # from the commandline it MUST be directly after the -e (-e2 not -e 2) # the long format is --extended=VAL # XXX: For some reason parsing of floats gives an error, so for now # ints and only fractions are allowed if [ "$2" ] && ! is_float "$2" && ! is_fraction "$2" ; then error "Extended multiplier must be a (positive) number (integer, float "\ "or fraction)." error " Got '$2'." exit $EX_USAGE fi if [ "$2" ]; then extended_factor="$2" else extended_factor=$DEFAULT_EXT_FACTOR fi shift ;; --mincho) font_filename=$FF_MINCHO ;; -I) # -I technically takes an optional argument (for future alternative # fonts) although it is documented as a two-letter option # Don't relay on using -I though, if I ever add a new alternative font # I might not allow it anymore if [ "$2" ] ; then # If an argument is passed, test it is one of the known ones case "$2" in k|j) ;; *) error "-I must be followed by j or k!" && exit $EX_USAGE ;; esac fi # It isn't tested for existence because it could also be a font # which convert would understand without giving the full path font_filename=$FF_MINCHO; shift ;; -O|--override) # Rough test if ! egrep -q '[a-zA-Z_]+=[^;]*' <<<"$2"; then error "Wrong override format, it should be variable=value. Got '$2'." exit $EX_USAGE fi override "$2" "command line" shift ;; -W) # Workaround mode, see wa_ss_* declarations at the start for details if [ "$2" != "o" ]; then error "Wrong argument. Use -Wo instead of -W$2." exit $EX_USAGE fi wa_ss_af='-ss ' wa_ss_be='' shift ;; -k|--funky) # Funky modes case $(tolower "$2") in p|polaroid) # Same as overlap + rotate + photoframe inf "Changed to polaroid funky mode." FILTERS_IND+=( 'filt_photoframe' 'filt_randrot' ) CSHEET_DELEGATE='csheet_overlap' # The timestamp must change location to be visible grav_timestamp=NorthWest ;; o|overlap) # Random overlap mode CSHEET_DELEGATE='csheet_overlap' grav_timestamp=NorthWest ;; r|rotate) # Random rotation FILTERS_IND+=( 'filt_randrot' ) ;; f|photoframe) # White photo frame FILTERS_IND+=( 'filt_photoframe' ) ;; i|film) inf "Enabled film mode." FILTERS_IND+=( 'filt_film' ) ;; x|random) # Random colours/fonts inf "Enabled random colours and fonts." randomize_look ;; *) error "Unknown funky mode. Got '$2'." exit $EX_USAGE ;; esac shift ;; -d|--disable) # Disable default features case $(tolower "$2") in # timestamp (no final s) is undocumented but will stay t|timestamps|timestamp) inf "Timestamps disabled." # TODO: Can array splicing be done in a saner way? declare -a tmp=${FILTERS_IND[@]} unset FILTERS_IND FILTERS_IND=${tmp[@]/filt_apply_stamp/} unset tmp ;; s|shadows|shadow) if [ $DISABLE_SHADOWS -eq 0 ]; then inf "Shadows disabled." DISABLE_SHADOWS=1 fi ;; *) error "Requested disabling unknown feature. Got '$2'." exit $EX_USAGE ;; esac shift ;; -q|--quiet) # -q to only show errors # -qq to be completely quiet if [ $verbosity -gt $V_ERROR ]; then verbosity=$V_ERROR else verbosity=$V_NONE fi ;; -D) # Repeat to just test consistency if [ $DEBUGGED -gt 0 ]; then exit ; fi inf "Testing internal consistency..." unit_test if [ $? -eq 0 ]; then warn "All tests passed" else error "Some tests failed!" fi DEBUGGED=1 warn "Command line: $0 $ARGS" title="$(basename "$0") $ARGS" ;; --) shift ; break ;; *) error "Internal error! (remaining opts: $@)" ; exit $EX_SOFTWARE ; esac shift done # Remaining arguments if [ ! "$1" ]; then show_help exit $EX_USAGE fi # }}} # Command line parsing # Test requirements test_programs || exit $EX_UNAVAILABLE # If -m is used then -S must be used if [ $manual_mode -eq 1 ] && [ -z $initial_stamps ]; then error "You must provide timestamps (-S) when using manual mode (-m)" exit $EX_USAGE fi set +e # Don't fail automatically for arg do process "$arg" ; done # vim:set ts=4 ai foldmethod=marker: #