#!/usr/bin/env bash # # $Rev$ $Date$ # # vcs # Video Contact Sheet *NIX: Generates contact sheets (previews) of videos # # Copyright (C) 2007, 2008, 2009, 2010 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 # # (Note: The references that used to be here have been moved to #+ ) declare -r VERSION="1.11" declare -r RELEASE=1 # {{{ # CHANGELOG # Last release changes: # (The full changelog can be found at ). # # 1.11: # * FEAT: Allow setting output filename. With extension will set output # format, without will inherit it. # * FEAT: Require mplayer OR ffmpeg instead of both. Having both is still # recommended for better results. # * FEAT: Safe mode, for files whose length doesn't get reported correctly. # Completely automated. # Number of tries can be increased with -Ws. Repeat to increase # further. Use -WS to do try as many times as possible. # Accuracy (stepping) can be increased with -Wp. Repeat to increase # accuracy. Decrease with -WP. # Can be deliberately disabled with -Wb to force processing of # broken files. *VCS WITH -Wb WILL FAIL ON BROKEN FILES* # * FEAT: Allow percentages in height. # * BUGFIX: Don't pass ms to mplayer. It ignores them anyway and in some # rare cases breaks the last capture (possibly due to the 5-frames # hack) # * BUGFIX: Honor detected aspect ratio if found # * BUGFIX: Try to detect files that might fail on the last capture and # trigger safe mode # * BUGFIX: Timestamps font was being ignored. As a side effect this produced # italiced timestamps in some systems. # * BUGFIX: Fixed obscure bug with safe_rename_pattern overrides # * COMPAT: Support for bash 2.05b. This will (probably) be the last version # capable of running under bash 2. # * DVD mode revamp # - Print title file size instead of disc size when possible # - Aspect ratio detection, if available # - Use of FFmpeg if available to get better information # - Mostly x-platform, only ISOs identification is a bit better in Linux # * Added FourCCs: 3IV1, 3IV2 (3ivx); s263 (H.263); mp4v, MP4V, H264 # (MPEG-4 and AVC in mov/mp4), VP6F (VP6 Flash Version), AMR # Video codec renamings: # - TechSmith codec name shortened to TechSmith SCC # - Raw RGB renamed to Raw video # * Help cleanup. The default help output is much shorter, the full text # can be displayed with --fullhelp. Also print the decoder choice near # the appropriate option (-M/-F) # * Added --anonymous to help (never was in it) # * Drop requirement on seq/jot and bc, replaced by inline awk # ... New requirement: Perl. It's required by POSIX anyway # * Adopt new/fixed numbering scheme # # * Check ImageMagick version (must decide which is the real minimum # required) # * Non-latin fonts revamp: # - -I no longer works alone (use -Ij or -Ik instead) # - -Ik, -Ij and --nonlatin try to pick an appropriate font automatically # - -I accepts a font name or font filename like # -Ij=Kochi-Mincho-Regular or # -Ij=/usr/share/fonts/truetype/kochi/kochi-mincho.ttf # * Deprecated options: # --shoehorn: Will be removed unless someone really needs it. # --mincho: Replaced by --nonlatin # * COSMETIC: # - Default font switched to DejaVu Sans. # Font sizes reduced to accomodate the new default. # Should fall back to a sane default if it's not available # - Much tighter padding # - Smaller timestamps font by default # - Print friendlier timestamp when a capture fails # - Print program signature to console without colour # - Use main font by default in timestamps # - Heading background colour toned down # - Added colourised output when tput is not capable (i.e. FreeBSD) # - Added prefixes when colour is not available for console output # - Don't print lsdvd error channel is DVD mode # - Suppress mv errors (e.g. over VFS being unable to preserve) # * UNDOCUMENTED/DEBUG: # - Allow stopping the main loop before cleaning up (--undocumented hang) # - Identification-only mode. Might be promoted to an actual feature # (--undocumented idonly) # - Allow setting ffmpeg and mplayer path (--undocumented set_ffmpeg and # set_mplayer) # - Allow disabling either mplayer of ffmpeg (as if they weren't # installed (--undocumented disable_ffmpeg and disable_mplayer) # - Added -Wc to disable console colour, repeat to disable prefixes # * INTERNAL: assert() # * INTERNAL: Cleanup: correctness checks converted to asserts, removal # of old dead code, fixed typos in comments # * Fixed various typos and cut out lines in CHANGELOG # * Minimum ImageMagick version set to 6.3.5-7 # * Better detection of requirements (e.g. disallow decoders without png # support) # * Allow overriding height, number of captures, interval, columns, and # padding # * Added -dp (--disable padding) equivalent to overriding HPAD to 0 # }}} # CHANGELOG set -e # Fail soon if this version of bash is too old for the syntax, don't expose bash to the newer # syntax # See the "Bash syntax notes" section for details [ "$BASH_VERSINFO" ] && { # Absolute minimum right now is 2.05b if [ "${BASH_VERSINFO[0]}" -le 2 ]; then # Only bash <=2 needs extra testing # I guess we can't expect any new bash2 release if [ "${BASH_VERSINFO[0]}" -lt 2 ] || ( [ "${BASH_VERSINFO[0]}" -eq 2 ] && [ "${BASH_VERSINFO[1]}" != '05b' ] ); then echo "Bash 2.05b or higher is required" >&2 exit 1 fi fi } # {{{ # TO-DO # TODO / FIXME: # * (1.12) Start replacing 'grep' with bash's '[[ =~ ]]'. Will break bash 2 compatibility for good # * [[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. => #+ SUS v2: egrep is deprecated, grep -E replaces it # * Change default DVD_TITLE to 0 # * Deprecations: # OPTION/VAR -> ALTERNATIVE DEPRECATED FROM VERSION REMOVAL ETA # --shoehorn -> --undocumented shoehorn 1.11 1.12 # --undocumented shoehorn -> NONE 1.12 1.13 or 1.14 # --funky -> --profile ? ?+1 # MIN_LENGTH_FOR_END_OFFSET -> none 1.11 1.12 or 1.13 # * No warning shown in 1.11. 1.12 will switch back default end offset either to 0 or a # percentage *and* use it always (disable with -E0) # end offset -> none ? ?+1 # --mincho -> --nonlatin 1.11 1.12 # * Better check for coherence of overrides # * Variables cleanup: # Variables will use a more uniform scheme, with prefixes where appropriate: # - INTERNAL_*: Used internally to adapt messages and the like to the input # - UNDFLAG_*: Undocumented flags. Used internally to keep track of undocumented modes (-Z) # - USR_*: Holds values of variables as set by the user, either from overrides or from the # command-line # }}} # TO-DO # {{{ # 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 config, 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 ASPECT=8 VCNAME=9 ACNAME=10 # 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 # }}} # End of constants # {{{ # Override-able variables # GETOPT must be correctly set or the script will fail. # It can be set in the configuration files if it isn't in the path or # the first getopt in the path isn't the right version. # A check will be made and a warning with details shown if required. declare GETOPT=getopt # 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='#afcd7a' # Background for meta info (size, codec...) declare bg_sign=SlateGray #'#a2a9af' # Background for signature declare bg_title=White # Background for the title (see -T) declare bg_contact=White # Background for the captures declare bg_tstamps='#000000aa' # Background for the timestamps box 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 for the title # Fonts, use identify -list font to get the list, up to IM 6.3.5-7 was '-list type' [[IM1]] # If a font is not available IM will pick a sane default. In theory it will be silent # although in practica it prints an error declare font_tstamps=DejaVu-Sans-Book # Used for timestamps over the thumbnails declare font_heading=DejaVu-Sans-Book # Used for the meta info heading 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 for the title (see -T) # Font sizes, in points declare -i pts_tstamps=14 # Used for the timestamps declare -i pts_meta=14 # Used for the meta info heading declare -i pts_sign=10 # Used for the signature declare -i pts_title=33 # Used for the title (see -T) # See --shoehorn declare shoehorned= # See -E / $end_offset declare -i DEFAULT_END_OFFSET=60 # If the video is less than this length, end offset won't be used at all declare MIN_LENGTH_FOR_END_OFFSET=19m30s # 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 # Verbosity level so far from the command line can only be muted (see -q) # it can be overridden, though declare -i verbosity=$V_ALL # Set to 1 to disable colours in console output declare -i plain_messages=0 # See coherence_check for more details declare -i DISABLE_SHADOWS=0 declare -i DISABLE_TIMESTAMPS=0 # Introduced in 1.0.7b, revamped in 1.11: # This font is used to display international names (i.e. CJK names) correctly # Help from users actually needing this would be appreciated :) # This variable is filled either automatically through the set_extended_font() #+function (and option -Ij) or manually (with option -Ij=MyFontName) # The automatic picks a semi-random one from the fonts believed to support CJK/Cyrillic #+characters. declare FONT_MINCHO= # Filename or font name as known to ImageMagick (identify -list font) # Output of capturing programs is redirected here declare stdout=/dev/null stderr=/dev/null declare -i DVD_MODE=0 DVD_TITLE=1 declare DVD_FILE= # Override-able since 1.11: # Height of the thumbnails, by default use same as input declare th_height= # *WILL CHANGE NAME* declare interval=$DEFAULT_INTERVAL # Interval of captures (=numsecs/numcaps) declare -i numcaps=$DEFAULT_NUMCAPS # Number of captures (=numsecs/interval) # This is the horizontal padding added to each capture. # Beware when changing this since extended set's alignment might break. # When shadows are enabled this is ignored since they already add padding. declare -i HPAD=2 # *WILL CHANGE NAME* declare -i cols=$DEFAULT_COLS # Number of output columns # }}} # End of override-able variables # {{{ # Variables # Options and other internal usage variables, no need to mess with this! 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 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 # Identification output from ffmpeg and mplayer for the current video declare FFMPEG_CACHE= declare MPLAYER_CACHE= # This holds the parsed identification values, see also the Indexes in VID # (defined in the constants block) declare -a VID=( ) # These variables will hold the output of tput, used # to colourise feedback declare prefix_err= prefix_inf= prefix_warn= suffix_fback= # 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 # Transformations/filters # Operations are decomposed into independent optional steps, this allows # to add some intermediate steps (e.g. polaroid/photo mode's frames) # 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 (currently deprecated) # * 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, [context, [index]] ) # They're executed in order by filter_vidcap() declare -a FILTERS_IND=( 'filt_resize' 'filt_apply_stamp' 'filt_softshadow' ) # Deprecated: 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 # Sets which function is used to obtain random numbers valid values are # bashrand and filerand. # Setting it manually will break it, calling with -R changes this to filerand. # See rand() for an explanation declare RANDFUNCTION=bashrand # Which file are we working on (i.e. how many times has process() been called) declare -i FILEIDX=0 # Names for output files, each index is a file name, an empty index will use # the input file and append an extension to it declare -a OUTPUT_FILES=( ) # Mplayer and FFmpeg binaries. Will be detected. # Don't set manually, if you need to override set the path temporarily, e.g.: # $ env PATH=/whatever:$PATH vcs ... # or use the undocumented (and unchecked!) appropriate option: # $ vcs --undocumented set_ffmpeg=/mypath/ffmpeg declare MPLAYER= declare FFMPEG= # When set to 1 the reported length by mplayer and ffmpeg won't be trusted # and will trigger some custom tests. # Enabled automatically on problematic files declare -i QUIRKS=0 # If the reported lengths differ by at least this much QUIRKS will be enabled declare QUIRKS_LEN_THRESHOLD=0.2 # When trying to determine the correct length, file will be probed each...: declare QUIRKS_LEN_STEP=0.5 # ~ 10 frames @ 20fps # Maximum number of seconds to "rewind" from reported length (after this # vcs surrenders but processing continues with a rewinded length) declare QUIRKS_MAX_REWIND=20 # Set when the console output will be in color. It doesn't control color! declare HAS_COLORS= declare -i multiple_input_files=0 # Internal counts, used only to adjust messages declare -i INTERNAL_WS_C=0 # -Ws count declare -i INTERNAL_WP_C=0 # -Wp count declare -i INTERNAL_MAXREWIND_REACHED=0 # More -Ws in the command-line won't help # }}} # 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' 'MIN_LENGTH_FOR_END_OFFSET' 'DEBUG' 'DISABLE_.*' 'th_height' 'interval' 'numcaps' 'HPAD' 'cols' # Note GETOPT doesn't make sense to be overridden from the command-line 'GETOPT' ) # 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 -a 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 end_offset=$DEFAULT_END_OFFSET } # 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=${ALLOWED_OVERRIDES[*]} compregex=${compregex// /|} # = s ' ' => '|' # Don't allow ';', FIXME: dunno how secure that really is... # FIXME: ...it doesn't really work 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=$(egrep -o '^[[:space:]]*[a-zA-Z0-9_]*=.' <<<"$o" | cut -d'=' -f1 | egrep -o '[^ ]*') local varval=$(egrep -o '^[^=]*=.*' <<<"$o" | cut -d'=' -f2-) # FIXME: Security! local curvarval= eval curvarval='$'"$varname" if [ "$curvarval" == "$varval" ]; then warn "Ignored override '$varname' (already had same value)" else eval "$varname=\"$varval\"" eval "USR_$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 is a valid percentage (xx% or xx.yy%) # is_percentage($1 = input) is_percentage() { egrep -q '^([0-9]+\.?([0-9])?+|(\.[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() { awkex "int(${*//[ ,]/ * }+0.5)" # ' ' = ',' => '*' } # Like rmultiply() but always rounded upwards ceilmultiply() { # TODO: breaks with $@. Why? awkex "int(${*//[ ,]/ * }+0.99999)" # ' ' = ',' => '*' } # Basic mathematic stuff # min($1 = operand1, $2 = operand2) min() { awk "BEGIN { if (($1) < ($2)) print ($1) ; else print ($2) }" ; } max() { awk "BEGIN { if (($1) > ($2)) print ($1) ; else print ($2) }" ; } abs() { awk "BEGIN { if (($1) < (0)) print (($1) * -1) ; else print ($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='!=' ;; *) assert $LINENO "[ \"'$1' '$2' '$3'\" ] && false" && return $EX_SOFTWARE esac # Empty operands if [ -z "$1" ] || [ -z "$3" ]; then assert $LINENO "[ \"'$1' '$2' '$3'\" ] && false" else awk "BEGIN { if ($1 $op $3) exit 0 ; else exit 1 }" fi } # Keep a number of decimals *rounded* keepdecimals() { local N="$1" D="$2" awk "BEGIN { printf \"%.${D}f\", (($N)+0) }" } # Keep a number of decimals, last decimal rounded to lower keepdecimals_lower() { grep -q '\.' <<<"$1" || { echo "$1" ; return ; } local D=${1/#*.} # Decimals only echo ${1/%.*}.${D:0:$2} # Integer part + . + Number of decimals } # Evaluate in AWK. Intended for arithmetic operations. #+Keep decimals. I.e. 5 = 5.000000... # awkex($1 = expression) awkexf() { # By default awk prints in compact form (scientific notation and/or up to 6 digits/decimals), # printf is used to avoid this, TODO: Is there any direct way? # .%20f is clearly overkill but matches the all code (default bc -l) awk "BEGIN { printf \"%.20f\", ($1)+0 }" } # Evaluate in AWK. Intended for arithmetic operations. #+Use default output. I.e. 5 = 5 # awkex($1 = expression) awkex() { awk "BEGIN { print ($1)+0 }" } # converts spaces to newlines in a x-platform way [[FNL]] # stonl($1 = string) stonl() { awk '{gsub(" ", "\n");print}' <<<"$1" | egrep -v '^$' } # Converts newlines to spaces portably # nltos($1 = string) nltos() { awk '{printf "%s ",$0}' <<<"$1" | sed 's/ *//' } # bash version of ord() [[ORD]] # prints the ASCII value of a character ord() { printf '%d' "'$1" } # Get file extension filext() { grep -o '\.[^.]*$' <<<"$1" | cut -d. -f2 } # Wrapper around $RANDOM, not called directly, wrapped again in rand(). # See rand() for an explanation. bashrand() { echo $RANDOM } # Prepares for "filerand()" calls # File descriptor 7 is used to keep a file open, from which data is read # and then transformed into a number. # init_filerand($1 = filename) init_filerand() { # [[FD1]], [[FD2]] test -r "$1" exec 7<"$1" # closed in exithdlr } # Produce a (not-really-)random number from a file, not called directly wrapped # in rand() # Note that once the file end is reached, the random values will always # be the same (hash_string result for an empty string) filerand() { local b= # "read 5 bytes from file descriptor 7 and put them in $b" read -n5 -u7 b hash_string "$b" } # Produce a random number # $RANDFUNCTION defines wich one to use (bashrand or filerand). # Since functions using random values are most often run in subshells # setting $RANDOM to a given seed has not the desired effect. # filerand() is used to that effect; it keeps a file open from which bytes # are read and not-so-random values generated; since file descriptors are # inherited, subshells will "advance" the random sequence. # Argument -R enables the filerand() function rand() { $RANDFUNCTION } # produces a numeric value from a string hash_string() { local HASH_LIMIT=65536 local v="$1" local -i hv=15031 local c= if [ "$v" ]; then for i in $(seqr 0 ${#v} ); do c=$( ord ${v:$i:1} ) hv=$(( ( ( $hv << 1 ) + $c ) % $HASH_LIMIT )) done fi echo $hv } # Applies the Pythagorean Theorem # pyth_th($1 = cathetus1, $2 = cathetus2) pyth_th() { awkex "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() { trace $FUNCNAME $@ # eval it even if it's numeric to strip leading zeroes. Note the quoting if is_number "$1" ; then awkex "\"$1\"" ; return 0 ; fi local s=$(tolower "$1") t r n # Only allowed characters if ! egrep -qi '^[0-9smh.]+$' <<<"$s"; then return $EX_USAGE; fi # Two consecutive dots are no longer accepted if egrep -q '\.\.' <<<"$s" ; then return $EX_USAGE fi # Newer parsing code: replaces units by a product # and feeds the resulting string to awk for evaluation # Note leading zeroes will lead awk to believe they are octal numbers # as a quick and dirty fix I'm just wrapping them in quotes, forcing awk # to re-evaluate them, which appears to be enough to make them decimal. # This is the only place where leading zeroes have no meaning. # Split into lines of time + unit: t= for item in $(echo "$s" | grep -o '[0-9]*[hms]') ;do n="\"$(echo $item | grep -o '[0-9]*')\"" # Number, quoted t=$t$n$(echo $item | grep -o '[hms]') # + Number + Unit done # Split all ms or s.ms for item in $(echo "$s" | grep -o '[0-9]*\.[0-9]*') ;do t="${t}\"$item\" + " done # Seconds without unit. They must be preceded by h, m or s at this point local secs=$(echo $s | egrep -o '.?[0-9]*$') # When preceded by '.', they're ms [ "$secs" ] && grep -q '\.'<<<"$secs" && secs= # Quote and addition. Note BSD grep/egrep wants the anchor ($) or won't match [ "$secs" ] && secs=" \"$(egrep -o '[0-9]*$'<<<"$secs")\" + " t=${t//h/ * 3600 + } t=${t//m/ * 60 + } t=${t//s/ + } t="$t$secs" t=${t/% + /} # Strip empty addition r=$(awkex "$t" 2>/dev/null) # Negative and empty intervals assert $LINENO "[ '$r' ] && [ '$t' ]" assert $LINENO "fptest $r -gt 0" 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 -format '%w' "$1" } # Get Image Height # imh($1 = file) imh() { identify -format '%h' "$1" } # Prints a number of seconds in a more human readable form # e.g.: 3600 becomes 1:00:00 # pretty_stamp($1 = seconds) pretty_stamp() { assert $LINENO "is_float '$1'" # Fully implemented in AWK to discard bc. # As a bonus now it's much faster and compact awk "BEGIN { t=$1 ; NOTMS=($DEC_MPLAYER==$decoder); MS=(t - int(t)); h=int(t / 3600); t=(t % 3600); m=int(t / 60); t=(t % 60); s=t if (h != 0) h=h\":\" ; else h=\"\" if (NOTMS!=1) ms=sprintf(\".%02d\", int(MS*100+0.5)); printf \"%s%02d:%02d%s\", h, m, s, ms }" # Note the rounding applied to $MS, it is required to match the precission passed on # to ffmpeg } # Prints a given size in human friendly form get_pretty_size() { local bytes=$1 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 } # 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_file_size($1 = file) get_pretty_file_size() { local f="$1" local bytes=$(get_file_size "$f") get_pretty_size "$bytes" } # Clean $safe_rename_pattern, which is override-able # Since safe_rename() is called from $() it won't be able to affect global variables directly sanitise_rename_pattern() { 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 # Hashmarks will break the regex used in safe_rename() if grep -q '#' <<<"$safe_rename_pattern" ; then warn "Illegal character \"#\" found in safe renaming pattern, resetting it" safe_rename_pattern=$DEFAULT_SAFE_REN_PATT fi } # mv quiet # Move a file, be quiet about errors. # Ownership preservation is a common error on vfs, for example mvq() { mv -- "$@" 2>/dev/null } # 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() { trace $FUNCNAME $@ local from="$1" local to="$2" # Output extension local ext=$(filext "$to") # Output filename without extension local b=${to%.$ext} local n=1 while [ -f "$to" ]; do # Only executes if $2 exists # Bash 2 and Bash 3 behave differently with substring replacement (${//}) and '%' # Sed is a safer bet 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 mvq "$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... # Neither busybox's nor BSD's du allow --bytes. # Note that using "ls -H" is not an option for portability reasons either. 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" } # Du replacement. This differs from get_file_size in that it takes multiple arguments dur() { for file in $@ ; do get_file_size "$file" done } # Gets the size of the dvd device, in DVD mode get_dvd_size() { # FIXME: Case sensivity might break with iso9660 if [ -f "$DVD_MOUNTP/VIDEO_TS/VTS_${DVD_VTS}_1.VOB" ]; then # Some VOBs available local vfiles="$DVD_MOUNTP/VIDEO_TS/VTS_${DVD_VTS}_*.VOB" # Print all sizes, each on a line, add '+' to the end of each line, add 0 to the end. local feed="$(dur "$DVD_MOUNTP/VIDEO_TS/VTS_${DVD_VTS}_"*".VOB" | cut -f1 | sed 's/$/ + /') 0" get_pretty_size $(awkex "$(nltos "$feed")") else echo "?" fi } is_linux() { uname -s | grep -iq '^Linux$' } # Get the mountpoint of a mounted image. # This only works on Linux. *BSD normal users aren't able to use mdconfig -l # Is there any better way? get_dvd_image_mountpoint() { if is_linux ; then local lodev=$(/sbin/losetup -j "$DVD_FILE" | cut -d':' -f1 | head -1) mount | grep "^$lodev " | cut -d' ' -f3 fi } # Tests the presence of all required programs # test_programs() test_programs() { local retval=0 last=0 local nopng=0 MPLAYER=$(type -pf mplayer) || true FFMPEG=$(type -pf ffmpeg) || true # Test we can actually use FFmpeg [ "$FFMPEG" ] && { # Newer FF has -codecs, -formats, -protocols, older has only -formats #+png is a codec so it's on different lists on newer and older if ! "$FFMPEG" -formats 2>/dev/null | grep -q 'EV.* png' && \ ! "$FFMPEG" -codecs 2>/dev/null | grep -q 'EV.* png' ; then warn "FFmpeg can't output to png, won't be able to use it." FFMPEG='' nopng=1 fi } # Same for Mplayer [ "$MPLAYER" ] && { if ! "$MPLAYER" -vo help 2>&1 | grep -q 'png' ; then warn "MPlayer can't output to png, won't be able to use it." MPLAYER='' nopng=1 fi } [ "$MPLAYER" ] || [ "$FFMPEG" ] || { local pngwarn= [ $nopng -eq 1 ] && pngwarn=', with PNG output support,' error "mplayer and/or ffmpeg$pngwarn are required!" let 'retval++,1' } if [ $decoder -eq $DEC_FFMPEG ] && [ -z "$FFMPEG" ]; then decoder=$DEC_MPLAYER elif [ $decoder -eq $DEC_MPLAYER ] && [ -z "$MPLAYER" ]; then decoder=$DEC_FFMPEG fi # awk is required by SUS/POSIX but just to be sure... for prog in convert montage identify mktemp grep egrep cut sed awk ; do if ! type -pf "$prog" ; then error "Required program $prog not found!" let 'retval++,1' fi >/dev/null done # TODO: [[x2]] # Early exit [ $retval -eq 0 ] || return $EX_UNAVAILABLE # ImageMagick version. 6 is a must, I'm probably using some # features that require a higher minor version # Versions tested: # * Fedora 9: IM 6.4.0 local ver ver=$(identify -version | head -n1 | grep -o 'ImageMagick\s*[^ ]*' |\ cut -f 2 -d' ') if [ "$ver" ]; then local verx=${ver//-/.}.0 # Extra .0 in case rev doesn't exist local major=$(cut -d'.' -f1 <<<"$verx") local minor=$(cut -d'.' -f2 <<<"$verx") local micro=$(cut -d'.' -f3 <<<"$verx") local rev=$(cut -d'.' -f4 <<<"$verx") local serial=$(( $major * 100000 + $minor * 10000 + $micro * 100 + $rev)) if [ "$serial" -lt 630507 ]; then error "ImageMagick 6.3.5-7 or higher is required. Found $ver." ; let 'retval++,1' fi else error "Failed to check ImageMagick version." let 'retval++,1' fi [ $retval -eq 0 ] || return $EX_UNAVAILABLE } # Test wether $GETOP is a compatible version; try to choose an alternate if # possible choose_getopt() { if ! type -pf $GETOPT ; then # getopt not in path error "Required program getopt not found!" return $EX_UNAVAILABLE fi >/dev/null local goe= gor=0 # Try getopt. If there's more than one in the path, try all of them for goe in $(type -paf $GETOPT) ; do "$goe" -T || gor=$? if [ $gor -eq 4 ]; then # Correct getopt found GETOPT="$goe" break; fi done >/dev/null if [ $gor -ne 4 ]; then error "No compatible version of getopt in path, can't continue." error " For details on how to correct this problems, see " return $EX_UNAVAILABLE fi return 0 } # 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 VCSTEMPDIR unset TEMPSTUFF ; declare -a TEMPSTUFF } # Exit callback. This function is executed on exit (correct, failed or # interrupted) # exithdlr() exithdlr() { # I don't think that's really required anyway if [ "$RANDFUNCTION" == "filerand" ]; then 7<&- # Close FD 7 fi 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 [ $plain_messages -eq 0 ] && echo -n "$prefix_err" # 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$suffix_fback" 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 [ $plain_messages -eq 0 ] && echo -n "$prefix_warn" echo "$1$suffix_fback" fi >&2 } # # Print an informational message # inf($1 = text) inf() { if [ $verbosity -ge $V_INFO ]; then [ $plain_messages -eq 0 ] && echo -n "$prefix_inf" echo "$1$suffix_fback" 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 } # # Tests if the filter chain contains the provided filter # has_filter($1 = filtername) has_filter() { local filter= ref=$1 for filter in ${FILTERS_IND[@]} ; do [ "$filter" == "$ref" ] || continue return 0 done return 1 } # # Enables prefixes in console output (instead of colour) set_feedback_prefixes() { prefix_err='[E] ' prefix_inf='[i] ' prefix_warn='[w] ' suffix_fback= } # # Initialises the variables affecting colourised feedback init_feedback() { HAS_COLORS= # tput might be preferable (Linux: man console_codes), but it doesn't # work on FreeBSD to set colors # Is tput available? if type -pf tput >/dev/null ; then # Is it able to set colours? if tput bold && tput setaf 0 >/dev/null && tput sgr0 ; then prefix_err=$(tput bold; tput setaf 1) prefix_warn=$(tput bold; tput setaf 3) prefix_inf=$(tput bold; tput setaf 2) suffix_fback=$(tput sgr0) HAS_COLORS="yes" fi fi if [ -z "$HAS_COLORS" ]; then # tput was not an option, let's try ANSI escape codes instead [[AEC]] # TODO: Detect support # Alternatively: $ perl -e 'print "\e[31m\e[1m"' # echo -e is not portable but echo $'' is bash-specific so it should be fine... # except when ANSI escape codes aren't supported of course prefix_err=$(echo $'\033[1m\033[31m') prefix_warn=$(echo $'\033[1m\033[33m') prefix_inf=$(echo $'\033[1m\033[32m') suffix_fback=$(echo $'\033[0m') HAS_COLORS="yes" fi # Finally, if there's no colour support, use prefixes instead if [ -z "$HAS_COLORS" ]; then set_feedback_prefixes fi } # # seq replacement # seq is not always present, jot is an alternative on FreeBSD. Instead, this is # a direct replacement # Note pure bash is *slower* than the awk (or perl) version # seqr($1 = from, $2 = to, $3 = increment) seqr() { local from=$1 to=$2 inc=$3 [ "$inc" ] || inc=1 awk "BEGIN { for (i=$from;i<=$to;i+=$inc) print i }" } # # assertion operator # assert($1 = line, $* = code) # TODO: Limit usage to values that will expand correctly always (i.e. not with quotes) assert() { [ $RELEASE -eq 1 ] && return LINE=$1 shift eval "$@" || { error "Internal error at line $LINE: $@" exit $EX_SOFTWARE } } # }}} # Convenience functions # {{{ # Core functionality # Creates a new temporary directory # create_temp_dir() create_temp_dir() { trace $FUNCNAME $@ [ "$VCSTEMPDIR" ] && return 0 # 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. # Passing a full path template is more x-platform than using # -t / -p if [ -d /dev/shm ] && [ -w /dev/shm ]; then VCSTEMPDIR=$(mktemp -d /dev/shm/vcs.XXXXXX) else [ "$TMPDIR" ] || TMPDIR="/tmp" VCSTEMPDIR=$(env TMPDIR="$TMPDIR" mktemp -d "$TMPDIR/vcs.XXXXXX") fi if [ ! -d "$VCSTEMPDIR" ]; then error "Error creating temporary directory" return $EX_CANTCREAT fi TEMPSTUFF=( "${TEMPSTUFF[@]}" "$VCSTEMPDIR" ) } # Resolve path. Realpath is not always available and readlink [[LC]] behaves differently in # GNU and BSD. FIXME: Has AWK or bash something similar? This is the only place requiring perl! realpathr() { perl -e "use Cwd qw(realpath);print realpath('$1')" } # Create a new temporal file and print its filename # new_temp_file($1 = suffix) new_temp_file() { trace $FUNCNAME $@ local r=$(env TMPDIR="$VCSTEMPDIR" mktemp "$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=( "${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 + ( $(rand) % $ncolours ) )) convert -list color | sed -n "${lineno}{p;q;}" | cut -d' ' -f1 # [[R1#11]] } else # Pseudo-random mode, WIP! randccomp() { # colours are in the 0..65535 range, while RANDOM in 0..32767 echo $(( $(rand) + $(rand) + ($(rand) % 1) )) } randcolour() { echo "rgb($(randccomp),$(randccomp),$(randccomp))" } fi # Older IM output was pretty different. Since this is a mode used for testing # I don't believe it's worth the effort to get it always right # This used to be -list type. Was this an older IM version or a bug in vcs? local nfonts=$(convert -list font | grep '^\s*Font:' | wc -l) randfont() { lineno=$(( $(rand) % $nfonts )) convert -list font | grep -o 'Font:.*' | sed -n "${lineno}{p;q;}" | cut -d' ' -f2 } 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=$(awkex "$end - $st") if fptest "$runlen" -lt $(get_interval "$MIN_LENGTH_FOR_END_OFFSET") ; then # Min length to use end offset not met, it won't be used inf "End offset won't be used, video too short." eo=0 elif 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 for the video, 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=$(awkexf "(($end-$st)/2 + 1)") else inc=$(awkexf "(($end-$eo-$st)/$tcnumcaps)") fi else error "Internal error" return $EX_SOFTWARE fi # Keep three decimals, round to lower to avoid exceeding the video length inc=$(keepdecimals_lower $inc 3) 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 local bound=$(awkexf "$end - $eo") while fptest $stamp -le "$bound"; do assert $LINENO fptest $stamp -ge 0 LTC=( "${LTC[@]}" "$stamp" ) stamp=$(keepdecimals_lower $(awkexf "$stamp + $inc") 3) done unset LTC[0] # Discard initial cap (=$st) TIMECODES=( ${TIMECODES[@]} ${LTC[@]} ) # Don't quote or extra empty stamp! } # 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 $@ 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 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 with ffmpeg # capture_ffmpeg($1 = inputfile, $2 = outputfile, $3 = timestamp[, $4 = extra opts]) capture_ffmpeg() { local f=$1 local o=$2 local ts=$3 # 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/ / $ts} -i "$f" ${wa_ss_af/ / $ts} -an \ -dframes 1 -vframes 1 -vcodec png \ -f rawvideo $4 $shoehorned "$o" >"$stdout" 2>"$stderr" [ -f "$o" ] && [ "0" != "$(du "$o" | cut -f1)" ] } # Capture a frame with mplayer # capture_mplayer($1 = inputfile, $2 = UNUSED, $3 = timestamp[, $4 = extra opts]) capture_mplayer() { # Note mplayer CAN'T set the output filename local f="$1" local o=00000005.png local ts=$3 # No point in passing ms to mplayer ts=$(cut -d'.' -f1 <<<"$ts") # Capture 5 frames and drop the first 4, fixes a weird bug/feature of mplayer ([M1]) { if [ $DVD_MODE -eq 1 ]; then "$MPLAYER" -sws 9 -ao null -benchmark -vo "png:z=0" -quiet \ -frames 5 -ss "$ts" $shoehorned -dvd-device "$DVD_FILE" \ $4 "dvd://$DVD_TITLE" else "$MPLAYER" -sws 9 -ao null -benchmark -vo "png:z=0" -quiet \ -frames 5 -ss "$ts" $4 $shoehorned "$f" fi } >"$stdout" 2>"$stderr" rm -f 0000000{1,2,3,4}.png # Remove the first four [ -f "$o" ] && [ "0" != "$(du "$o" | cut -f1)" ] } # 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_mplayer "$f" 'IGNOREME' "$stamp" elif [ $decoder -eq $DEC_FFMPEG ]; then # FIXME: ffmpeg can put the temporary file anywhere capture_ffmpeg "$f" "$VIDCAPFILE" "$stamp" else error "Internal error!" return $EX_SOFTWARE fi || true if [ ! -f "$VIDCAPFILE" ] || [ "0" == "$(du "$VIDCAPFILE" | cut -f1)" ]; then [ $decoder -eq $DEC_MPLAYER ] && stamp=${stamp/%.*} error "Failed to capture frame at $(pretty_stamp $stamp) (${stamp}s)" return $EX_SOFTWARE fi return 0 } # Applies all individual vidcap filters # filter_vidcap($1 = filename, $2 = timestamp, $3 = width, $4 = height, $5 = context, $6 = index[1..]) 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" "$5" "$6" ) -flatten " done local t=$(new_temp_file .png) eval "convert '$1' $cmdopts '$t'" # If $t doesn't exist returns non-zero [ -f "$t" ] && mvq "$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, $5 = context, $6 = index) filt_apply_stamp() { trace $FUNCNAME $@ local filename=$1 timestamp=$2 width=$3 height=$4 context=$5 index=$6 local pts=$pts_tstamps if [ $height -lt 200 ]; then pts=$(( $pts_tstamps / 3 )) elif [ $height -lt 400 ]; then pts=$(( $pts_tstamps * 2 / 3 )) fi # If the size is too small they won't be readable at all # With the original font 8 was the minimum, with DejaVu 7 is readable if [ $pts -le 7 ]; then pts=7 if [ $index -eq 1 ] && [ $context -ne $CTX_EXT ]; then warn "Very small timestamps in use. Disabling them with -dt might be preferable" fi fi # The last -gravity None is used to "forget" the previous gravity (otherwise it would # affect stuff like the polaroid frames) echo -n " \( -box '$bg_tstamps' -fill '$fg_tstamps' -stroke none -pointsize '$pts' " echo -n " -gravity '$grav_timestamp' -font '$font_tstamps' -strokewidth 3 -annotate +5+5 " echo " ' $(pretty_stamp $stamp) ' \) -flatten -gravity None " } # Apply a framed photo-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 ) ) )) # The border is relative to the input size (since 1.0.99), with a maximum of 6 # Should probably be bigger for really big frames # Note that only images below 21600px (e.g. 160x120) go below a 6px border local border=$(( ($3*$4) / 3600 )) [ $border -lt 7 ] || border=6 echo -n "-bordercolor white -border $border -bordercolor grey60 -border 1 " } filt_softshadow() { # Before this was a filter, there was the global (montage) softshadow (50x2+10+10) and the # photoframe inline softshadow 60x4+4+4 echo -n "\( -background black +clone -shadow 50x2+4+4 -background none \) +swap -flatten -trim +repage " } # Apply a polaroid-like border effect # Based on filt_photoframe(), with a bigger lower border # filt_polaroid($1 = filename, $2 = timestamp, $3 = width, $4 = height) filt_polaroid() { trace $FUNCNAME $@ # local file="$1" ts=$2 w=$3 h=$4 local border=$(( ($3*$4) / 3600 )) # Read filt_photoframe for details [ $border -lt 7 ] || border=6 echo -n "-bordercolor white -mattecolor white -frame ${border}x${border} " # FIXME: This is rather ugly (double-flipping) there's sure a better way echo -n "\( -flip -splice 0x$(( $border*5 )) \) " echo "-flip -bordercolor grey60 -border 1 +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=$(( ($(rand) % 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="$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= vpad= splice= # The shadows already add a good amount of padding if has_filter filt_softshadow ; then hpad=0 vpad=0 splice=5x10 else hpad=$HPAD vpad=$HPAD splice=0x8 fi montage -background Transparent "$@" -geometry +$hpad+$vpad -tile "$cols"x "$output" convert "$output" -background Transparent -splice $splice "$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 $(seqr 1 $numrows) ; do col=0 rowfile=$(new_temp_file .png) rowfiles=( "${rowfiles[@]}" "$rowfile" ) accoffset=0 cmdopts= # This command is pretty time-consuming, let's make it in a row # Base canvas # Integrated in the row creation since 1.0.99 # Step through vidcaps (col=[0..cols-1]) for col in $(seqr 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 cmdopts="$cmdopts '$1' -geometry +${accoffset}+0 -composite " offset=$(( $minoffset + ( $(rand) % $maxoffset ) )) let 'accoffset=accoffset + w - offset' shift done inf "Composing overlapped row $row/$numrows..." eval convert -size ${canvasw}x${canvash} xc:transparent -geometry +0+0 "$cmdopts" -trim +repage "'$rowfile'" >&2 done inf "Merging overlapped rows..." output=$(new_temp_file .png) cmdopts= accoffset=0 local h for row in "${rowfiles[@]}" ; do w=$(imw "$row") h=$(imh "$row") minoffset=$(( $h / 8 )) maxoffset=$(( $h / 4 )) offset=$(( $minoffset + ( $(rand) % $maxoffset ) )) # The row is also offset horizontally cmdopts="$cmdopts '$row' -geometry +$(( $(rand) % $maxoffset ))+$accoffset -composite " let 'accoffset=accoffset + h - offset' done # After the trim the image will be touching the outer borders and the heading and footer, # older versions (prior to 1.0.99) used -splice 0x10 to correct the heading spacing, 1.0.99 # onwards uses -frame to add spacing in all borders + splice to add a bit more space on the # upper border. Note splice uses the background colour while frame uses the matte colour eval convert -size ${canvasw}x$(( $canvash * $cols )) xc:transparent -geometry +0+0 \ $cmdopts -trim +repage -bordercolor Transparent -background Transparent -mattecolor Transparent \ -frame 5x5 -splice 0x5 "$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 I replace spaces by newlines local s=$1 stonl "$s" | sort -n | uniq } # Test the video at a given timestamp (to see if it can be reached) # See safe_length_measure() # probe_video($1 = input file, $2 = stamp) probe_video() { local f="$1" local ts="$2" local tempfile= local ret=0 # create_temp_dir should have been called # This time a resize filter is applied to the player to produce smaller # output if [ $decoder -eq $DEC_MPLAYER ]; then tempfile=00000005.png TEMPSTUFF=( "${TEMPSTUFF[@]}" "$tempfile" ) if ! capture_mplayer "$f" "IGNOREME" "$ts" "-vf scale=96:96"; then ret=1 fi elif [ $decoder -eq $DEC_FFMPEG ]; then tempfile=$(new_temp_file '-safelen.png') if ! capture_ffmpeg "$f" "$tempfile" "$ts" "-s 96x96"; then ret=1 fi else assert $LINENO false ret=1 fi rm -f "$tempfile" return $ret } # Try to guess a correct length for the video, taking the reported lengths a # starting point # safe_length_measure($1 = filename) safe_length_measure() { trace $FUNCNAME $@ local f="$1" local len=${VID[$LEN]} local tempfile= local newlen=$len local capturefn= create_temp_dir if probe_video "$1" $len ; then inf " File looks fine, suspicion withdrawn" echo "$len" return 0 else # Can't seek to the very end, adjust warn "Starting safe length measuring (this might take a while)..." local maxrew=$(min $QUIRKS_MAX_REWIND $(awkex "int($len)")) # At most we'll rewind 20 seconds # -1 (-WS) => Rewind up to the start # Might be -2, -4, ... e.g. (-WS -Ws) if fptest $maxrew -ge $len || fptest "$maxrew" -lt 0 ; then maxrew=$len INTERNAL_MAXREWIND_REACHED=1 fi for rew in $(seqr $QUIRKS_LEN_STEP $maxrew $QUIRKS_LEN_STEP); do newlen=$(keepdecimals_lower $(awkex "$len - $rew") 3) warn " ... trying $newlen" if probe_video "$f" "$newlen" ; then echo $newlen return 0 fi done fi # Hitting this line means we're doomed! return 1 } ##### {{{{ Codec names # Codecs TODO: Clean this # Translates an mplayer codec id/fourcc to its name get_vcodec_name() { local vcid="$1" local vcodec= # Video codec "prettyfication", see [[R2]], [[R3]], [[R4]] case "$vcid" in 0x10000001) vcodec="MPEG-1" ;; 0x10000002) vcodec="MPEG-2" ;; 0x00000000) vcodec="Raw video" ;; # How correct is this? avc1|H264) vcodec="MPEG-4 AVC" ;; # H264 is used in mov/mp4 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" ;; # 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 SCC" ;; VP6[012F]) 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" ;; 3IV2) vcodec="3ivx Delta 4.0" ;; # Rare but seen # 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" ;; s263) vcodec="H.263" ;; # 3GPP # Legacy(-er) codecs (haven't seen files in these formats in awhile) IV3[0-9]) vcodec="Indeo Video 3" ;; # FF only recognises IV31 MSVC) vcodec="Microsoft Video 1" ;; MRLE) vcodec="Microsoft RLE" ;; 3IV1) vcodec="3ivx Delta" ;; # "mp4v" is the MPEG-4 fourcc *in mov/mp4/3gp*; but I also found MP4V (Apple's iTunes sample) #mp4v|MP4V) vcodec="MPEG-4" ;; mp4v|MP4V) vcodec="MPEG-4" ;; # Synthetic, used for ffmpeg translations vcs_divx) vcodec="DivX ;-)" ;; *) # If not recognized fall back to FourCC vcodec="$vcid" ;; esac echo "$vcodec" } # Translates an FFmpeg codec id to an MPlayer codec id/fourcc # TODO: Clean this translate_ffmpeg_vcodec_id() { # The list of ffmpeg codecs might be retrieved by looking at the code but I #+simply used the ffmpeg -formats / ffmpeg -codecs command # Supported video decoders: $ ffmepg -codecs | grep '^ D.V' local vcid="$1" local mpid= case "$vcid" in mpeg1video) mpid="0x10000001" ;; # mpeg1video_vdpau? mpeg2video) mpid="0x10000002" ;; rawvideo) mpid="0x00000000" ;; # can't distinguish from I420 h264) mpid="avc1" ;; mjpeg) mpid="MJPG" ;; msmpeg4v1) mpid="MPG4" ;; msmpeg4v2) mpid="MP42" ;; theora) mpid="theo" ;; camtasia) mpid="tscc" ;; vp6|vp6a|vp6f) mpid="VP60" ;; # FIXME List of codec id's I translate but haven't test: # svq3, rv40, theora, camtasia, vp6* # MPlayer uses uppercase while FFmpeg uses lowercase rv10|rv20|rv30|rv40|svq1|svq3|wmv1|wmv2|wmv3) mpid=$(echo $vcid | tr '[a-z]' '[A-Z]') ;; # FFmpeg doesn't print FourCC's so there's some codecs that can't be told apart msmpeg4) mpid="vcs_divx" ;; # DIV3 = DIV4 = MP43 # XVID = DIVX = DX50 = FMP4 = ... = mpeg4 mpeg4) mpid="mp4v" ;; # Take advantage of an unamed MPEG-4 h263) mpid="s263" ;; vc1) mpid="WVC1" ;; # In FF: WMVA = vc1 # Not supported (ff just prints the FourCC) # IV4*, vp4 vp3) mpid="VP30" ;; vp5) mpid="VP50" ;; # Legacy(-er) codecs (haven't seen files in these formats in awhile) # MSVC? MRLE? indeo3) mpid="IV31" ;; *) # If not recognized fall back to FourCC mpid="$vcid" ;; esac echo $mpid } get_acodec_name() { local acid="$1" local acodec= if grep -q '[ -]' <<<"$acid" ; then # Won't be recognised anyway echo "$acid" return fi case "$(tolower "$acid")" 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, Ogg, probably others acodec='Vorbis' ;; qdm2) acodec="QDesign" ;; "") acodec="no audio" ;; samr) acodec="AMR" ;; # AMR-NB/AMR-WB? # 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="$acid" ;; esac echo "$acodec" } translate_ffmpeg_acodec_id() { local acid="$1" local mpid= # ffmpeg -codecs | grep ^\ D.A case "$acid" in mp3) mpid='85' ;; # Note FF can tell apart mp1/mp2 directly mp1) mpid='MPEG Layer I (MP1)' ;; mp2) mpid='MPEG Layer II (MP2)' ;; aac) mpid='mp4a' ;; # Can aac be MPEG2? wmav1) mpid='352' ;; wmav2) mpid='353' ;; wmapro) mpid='354' ;; # Actually WMA9 Professional ac3) mpid='8192' ;; # FF has a ton of pcm variants (sign, endianness, ...) pcm_*) mpid="1" ;; vorbis) mpid="vrbs" ;; qdm2) mpid="QDM2" ;; libopencore_amrnb) mpid="AMR-NB" ;; libopencore_amrwb) mpid="AMR-WB" ;; *) # If not recognized show audio id tag mpid="$acid" ;; esac echo "$mpid" } ##### }}}} # Codec names # Try to identify video properties using mplayer # Fills $MPLAYER_CACHE with the relevant output and $VID_MPLAYER with # the actual values. See identify_video() # mplayer_identify($1 = file) mplayer_identify() { trace $FUNCNAME $@ [ "$MPLAYER" ] || return local f="$1" local mi=( ) # Note to self: Don't change the -vc as it would affect $vdec if [ $DVD_MODE -eq 0 ]; then MPLAYER_CACHE=$("$MPLAYER" -benchmark -ao null -vo null -identify -frames 0 \ -quiet "$f" 2>/dev/null | grep ^ID) else MPLAYER_CACHE=$("$MPLAYER" -benchmark -ao null -vo null -identify -frames 0 \ -quiet -dvd-device $DVD_FILE dvd://$DVD_TITLE \ 2>/dev/null | grep ^ID) fi # Note the head -1! mi[$VCODEC]=$(grep ID_VIDEO_FORMAT <<<"$MPLAYER_CACHE" | head -1 | cut -d'=' -f2) # FourCC mi[$ACODEC]=$(grep ID_AUDIO_FORMAT <<<"$MPLAYER_CACHE" | head -1 | cut -d'=' -f2) mi[$VDEC]=$(grep ID_VIDEO_CODEC <<<"$MPLAYER_CACHE" | head -1 | cut -d'=' -f2) # Decoder (!= Codec) mi[$W]=$(grep ID_VIDEO_WIDTH <<<"$MPLAYER_CACHE" | head -1 | cut -d'=' -f2) mi[$H]=$(grep ID_VIDEO_HEIGHT <<<"$MPLAYER_CACHE" | head -1 | cut -d'=' -f2) mi[$FPS]=$(grep ID_VIDEO_FPS <<<"$MPLAYER_CACHE" | head -1 | cut -d'=' -f2) # For some reason my (one track) samples have two ..._NCH, first one 0 #+Also multichannel is detected as 2 ch mi[$CHANS]=$(grep ID_AUDIO_NCH <<<"$MPLAYER_CACHE"| grep -v '=0' | cut -d'=' -f2|head -1) if [ $DVD_MODE -eq 0 ]; then # For DVDs it prints ID_DVD_TITLE_x_LENGTH and ID_LENGTH. #+Both appear valid. mi[$LEN]=$(grep ID_DVD_TITLE_${DVD_TITLE}_LENGTH <<<"$MPLAYER_CACHE"| cut -d'=' -f2) [ "${mi[$LEN]}" ] || mi[$LEN]=$(grep ID_LENGTH <<<"$MPLAYER_CACHE"| head -1 | cut -d'=' -f2) else mi[$LEN]=$(grep ID_DVD_TITLE_${DVD_TITLE}_LENGTH <<<"$MPLAYER_CACHE"| head -1 | cut -d'=' -f2) fi # Voodoo :P Remove (one) trailing zero if [ "${mi[$FPS]:$(( ${#mi[$FPS]} - 1 ))}" == "0" ]; then mi[$FPS]="${mi[$FPS]:0:$(( ${#mi[$FPS]} - 1 ))}" fi mi[$ASPECT]=$(grep ID_VIDEO_ASPECT <<<"$MPLAYER_CACHE" | egrep -v '^0.0000$' | cut -d'=' -f2 | tail -1) # If none set, delete it [ "${mi[$ASPECT]}" ] && fptest "${mi[$ASPECT]}" -eq 0.0 && mi[$ASPECT]='' mi[$VCNAME]=$(get_vcodec_name "${mi[$VCODEC]}") if [ "${mi[$VDEC]}" == "ffodivx" ] && [ "${mi[$VCNAME]}" != "MPEG-4" ]; then mi[$VCNAME]="${mi[$VCNAME]} (MPEG-4)" elif [ "${mi[$VDEC]}" == "ffh264" ]; then # At least two different fourccs use h264, maybe more mi[$VCNAME]="${mi[$VCNAME]} (h.264)" fi mi[$ACNAME]=$(get_acodec_name "${mi[$ACODEC]}") if [ "${mi[$ACODEC]}" == "samr" ] ; then local adec=$(grep ID_AUDIO_CODEC <<<"$MPLAYER_CACHE" | head -1 | cut -d'=' -f2) if [ "$adec" == "ffamrnb" ]; then mi[$ACNAME]="AMR-NB"; fi fi # Array assignment VID_MPLAYER=("${mi[@]}") } # Try to identify video properties using ffmpeg # Fills $FFMPEG_CACHE with the relevant output and $VID_FFMPEG with # the actual values. See identify_video() # mplayer_identify($1 = file) ffmpeg_identify() { trace $FUNCNAME $@ [ "$FFMPEG" ] || return # (AFAIK) Can't use ffmpeg in DVD Mode #[ $DVD_MODE -eq 0 ] || return local f="$1" # DVD Devices *MUST* be mounted for the identifying process to even start assert $LINENO "[ $DVD_MODE -eq 0 ] || [ '$DVD_MOUNTP' ]" [ $DVD_MODE -eq 1 ] && [ "$DVD_DEVICE" ] && { local vfile="$DVD_MOUNTP/VIDEO_TS/VTS_${DVD_VTS}_1.VOB" if [ ! -r "$vfile" ]; then error "Failed to locale mounted DVD. Detection will be less accurate." return 0 # We can continue anyway fi f="$vfile" } local fi=( ) vs= as= obs= vsid= # FFmpeg is relatively new, introduced in 1.0.99 so it needs more testing FFMPEG_CACHE=$("$FFMPEG" -i "$f" -dframes 0 -vframes 0 /dev/null 2>&1 | egrep '(Stream|Duration:|^Seems)') # Only the first streams of each type are honored. FIXME: Add multi-audio support. vs=$(grep Stream <<<"$FFMPEG_CACHE" | grep 'Video:' | head -1) as=$(grep Stream <<<"$FFMPEG_CACHE" | grep 'Audio:' | head -1) obs=$(grep Seems <<<"$FFMPEG_CACHE") # Stream #0.0: Video: mpeg4, yuv420p, 624x352 [PAR 1:1 DAR 39:22], 23.98 tbr, 23.98 tbn, 24k tbc # New and old versions of ffmpeg changed their output considerably, e.g.: # (same file, Robotica_720.wmv) # New output: # Seems stream 1 codec frame rate differs from container frame rate: 1000.00 (1000/1) -> 23.98 (24000/1001) # [...] # Duration: 00:00:20.77, start: 3.000000, bitrate: 6250 kb/s # Stream #0.0(eng): Audio: wmapro, 48000 Hz, 6 channels, s16, 384 kb/s # Stream #0.1(eng): Video: wmv3, yuv420p, 1280x720, 6500 kb/s, 23.98 tbr, 1k tbn, 1k tbc # Old output: # Duration: 00:00:20.7, start: 3.000000, bitrate: 6250 kb/s # Stream #0.0: Audio: 0x0162, 48000 Hz, 5:1, 384 kb/s # Stream #0.1: Video: wmv3, yuv420p, 1280x720, 24.00 fps(r) # TODO: tbr is rounded to two decimals but the actual ratio is printed: # 24000/1001 = 23.97602 # (older ffmpeg prints 24 fps, 24/1 so no luck here # **Also seen**: (note the 'tb(r)') # Stream #0.1: Video: wmv3, yuv420p, 1440x1080 [PAR 4:3 DAR 16:9], 8000 kb/s, 23.98 tb(r) # **Also seen**: (VOB, latest ffmpeg as of this writing): # Stream #0.0[0x1e0]: Video: mpeg2video, yuv420p, 720x576 [PAR 64:45 DAR 16:9], 9800 kb/s, 23.53 fps, 25 tbr, 90k tbn, 50 tbc # **Also seen**: (DVB TS to DX50 in MKV), note the DAR mess, the second one is the correct one # Stream #0.0: Video: mpeg4, yuv420p, 640x326 [PAR 1:1 DAR 320:163], PAR 231:193 DAR 73920:31459, 25 fps, 25 tbr, 1k tbn, 25 tbc vsid=$(grep -o '#0.[0-9]' <<<"$vs" | cut -d'.' -f2) # Video Stream ID fi[$VCODEC]=$(egrep -o 'Video: [^,]*' <<<"$vs" | cut -d' ' -f2-) # ffmpeg's codec might contain spaces in some cases, i.e. iv4 in mov (see mplayer's bestiary) #+unless this turns out to be common I won't be handling it specially # Note unidentified audio codecs will be printed in hexadecimal fi[$ACODEC]=$(egrep -o 'Audio: [^,]*' <<<"$as" | cut -d' ' -f2) fi[$VDEC]='' # The comma is required for cases where the stream id is printed (in hex) fi[$W]=$(egrep -o ', [0-9]*x[0-9]*' <<<"$vs" | sed 's/^, //' | cut -dx -f1) fi[$H]=$(egrep -o ', [0-9]*x[0-9]*' <<<"$vs" | cut -dx -f2) # Newer CHANS and some older... fi[$CHANS]=$(egrep -o '[0-9]* channels' <<<"$as" | cut -d' ' -f1) # ...fallback for older if [ -z "${fi[$CHANS]}" ]; then local chans=$(egrep -o 'Hz, [^,]*' <<<"$as" | cut -d' ' -f2) case $chans in mono) fi[$CHANS]=1 ;; stereo) fi[$CHANS]=2 ;; 5.1|5:1) fi[$CHANS]=6 ;; # * *) ;; # Other layouts use 'N channels' # 5.1 was in the previous version (can't remember if it was empirical). esac fi # Newer FPS... # tbr/tbn/tbc explanation: tb stands for time base # n: AVStream, c: AVCodecContext, r: VideoStream (Guessed) # tbr is the best bet. Note it's common for WMVs to contains "1k tbn, 1k tbc" # tbr is rounded to two decimals, the values used to derived it might be # printed in a "Seems ..." line like the one in the example above so it # can be re-calculated. fi[$FPS]=$(egrep -o '[0-9]*\.?[0-9]*k? tb(r|\(r\))' <<<"$vs" | cut -d' ' -f1) # Let's convert e.g. 23.98 into 23.976...: if [ "${fi[$FPS]}" ] && grep -q '\.' <<<"${fi[$FPS]}" ; then # Decimals, see if we got better values available local vsobs=$(grep "stream $vsid" <<<"$obs") # Observations regarding video stream found if [ "$vsobs" ] && grep -q " -> ${fi[$FPS]} (.*)" <<<"$vsobs" ; then # FPS candidate local newfps=$(egrep -o -- '-> [^ ]* \([0-9]*/[0-9]*' <<<"$vsobs" | cut -d'(' -f2) is_fraction $newfps && fi[$FPS]=$(keepdecimals "$newfps" 3) fi fi # ...fallback for older. The older version I tried seems to round further, i.e. # 23.976 became 24 so no fix for this one if [ -z "${fi[$FPS]}" ]; then # No k suffix here, 1000 is 1000 fi[$FPS]=$(egrep -o '[0-9]*\.?[0-9]* fps' <<<"$vs" | cut -d' ' -f1) fi # Be consistent with mplayer's output: at least two decimals [ "${fi[$FPS]}" ] && { fi[$FPS]=$(keepdecimals "${fi[$FPS]}" 3) fi[$FPS]=${fi[$FPS]/%0} # Strip 0$ } fi[$LEN]=$(egrep -o 'Duration: [^,]*' <<<"$FFMPEG_CACHE" | cut -d' ' -f2) if [ "${fi[$LEN]}" == "N/A" ]; then # It might be unable to detect fi[$LEN]="" fi fi[$LEN]=$( get_interval $(echo "${fi[$LEN]}" | sed 's/:/h/' | sed 's/:/m/') ) # Aspect ratio in FFmpeg is only provided in newer ffmpeg # It might be calculated for files without one (which is ok anyway) # TODO: Replace tail -1 with some better option (see the double DAR example above) fi[$ASPECT]=$(egrep -o 'DAR [0-9]*:[0-9]*'<<<"$FFMPEG_CACHE" | tail -1 | cut -d' ' -f2 | sed 's#:#/#') # Due to calling ffmpeg on a single VOB when in DVD Device mode, the length will be partial [ $DVD_MODE -eq 1 ] && [ "$DVD_DEVICE" ] && fi[$LEN]='' fi[$VCNAME]=$(get_vcodec_name $(translate_ffmpeg_vcodec_id "${fi[$VCODEC]}")) fi[$ACNAME]=$(get_acodec_name $(translate_ffmpeg_acodec_id "${fi[$ACODEC]}")) VID_FFMPEG=("${fi[@]}") } # Use the available tools to identify video meta-data # fills $VID with the values # Return codes: # 3: Failed to detect length # 4: Failed to detect width or height # identify_video($1 = file) identify_video() { trace $FUNCNAME $@ local RET_NOLEN=3 RET_NODIM=4 [ "$MPLAYER" ] && mplayer_identify "$1" # ffmpeg_identify in DVD mode only works when the DVD is mounted: [ $DVD_MODE -eq 0 ] && [ "$FFMPEG" ] && ffmpeg_identify "$1" [ $DVD_MODE -eq 1 ] && [ "$FFMPEG" ] && [ "$DVD_MOUNTP" ] && ffmpeg_identify "$1" # Fail early if none detected length [ -z "${VID_MPLAYER[$LEN]}" ] && [ -z "${VID_FFMPEG[$LEN]}" ] && return $RET_NOLEN # Classic mode, use both mplayer and ffmpeg when available if [ "$MPLAYER" ] && [ "$FFMPEG" ]; then # By default take mplayer's values VID=("${VID_MPLAYER[@]}") # FFmpeg seems better at getting the correct number of FPS, specially with # WMVs, where mplayer often accepts 1000fps while ffmpeg notices the # inconsistency in container vs codec and guesses better, *but* it only # uses two decimals so 23.976 becomes 23.98. So it is only used when # the number of decimals seems right. # When a "Seems..." line is printed the correct FPS can be obtained though. [ -z "${VID_MPLAYER[$FPS]}" ] && VID[$FPS]=${VID_FFMPEG[$FPS]} [ "${VID_MPLAYER[$FPS]}" ] && [ "${VID_FFMPEG[$FPS]}" ] && { # Trust ffmpeg if it has three decimals OR if mplayer is probably-wrong local ffps=${VID_FFMPEG[$FPS]} echo $ffps | grep -q '\.[0-9][0-9][0-9]' && VID[$FPS]=$ffps || { fptest "${VID_MPLAYER[$FPS]}" -gt 500 && VID[$FPS]=$ffps } } # It doesn't appear to need any workarounds for num. channels either [ "${VID_FFMPEG[$CHANS]}" ] && VID[$CHANS]=${VID_FFMPEG[$CHANS]} [ "${VID_FFMPEG[$ASPECT]}" ] && VID[$ASPECT]=${VID_FFMPEG[$ASPECT]} # There's a huge inconsistency with some files, both mplayer vs ffmpeg # same application on different OSes local fflen=${VID_FFMPEG[$LEN]} mplen=${VID_MPLAYER[$LEN]} # Shorthands [ -z "$fflen" ] && fflen=0 # If both report 0, there's no good value... fptest "$fflen" -eq 0 && fptest "$mplen" -eq 0 && return $RET_NOLEN if [ $DVD_MODE -eq 0 ] && [ $QUIRKS -eq 0 ]; then # In DVD mode ffmpeg has no length # Quirks disabled, should be enabled? local delta=$(abs $(awkex "($fflen - $mplen)")) # If they don't agree, take the shorter as a starting point, #+if both are different than zero take min, if one of them is 0, take max to start if fptest "$fflen" -ne 0 && fptest "$mplen" -ne 0 ; then VID[$LEN]=$(min $fflen $mplen) else VID[$LEN]=$(max $fflen $mplen) delta=$QUIRKS_LEN_THRESHOLD # Ensure it's considered inconsistent fi # If they differ too much, enter safe mode. If one reports 0, they'll differ... # FIXME: If $decoder reports 0, can it seek?? if fptest "$delta" -ge $QUIRKS_LEN_THRESHOLD ; then warn "Found inconsistency in reported length. Safe measuring enabled." QUIRKS=1 fi fi elif [ "$MPLAYER" ]; then # Must do with mplayer only... VID=("${VID_MPLAYER[@]}") # Warn if a known pitfall is found # See above for 1000 fps [ "${VID[$FPS]}" == "1000.00" ] && \ warn "Possible inaccuracy in FPS detection." && \ warn " Install both mplayer and ffmpeg for better detection." # Number of channels 0 happened for WMA in non-x86 [ "${VID[$CHANS]}" == "0" ] && \ warn "Failed to detect number of audio channels." && \ warn " Install both mplayer and ffmpeg for better detection." elif [ "$FFMPEG" ]; then # Must do with mplayer only... VID=("${VID_FFMPEG[@]}") # So far I know of no weird results. Yet. else assert $LINENO 'false' fi # Ensure sanity of the most important values is_float "${VID[$LEN]}" || return $RET_NOLEN is_number "${VID[$W]}" && is_number "${VID[$H]}" || return $RET_NODIM if [ "$FFMPEG" ]; then # FPS at least with two decimals if [ $(awkex "int(${VID[$FPS]})") == ${VID[$FPS]} ]; then VID[$FPS]="${VID[$FPS]}.00" fi fi local mfps="${VID_MPLAYER[$FPS]}" if [ $QUIRKS -eq 0 ] && [ "$MPLAYER" ] && fptest "$mfps" -eq 1000 ; then warn "Suspect file. Safe measuring enabled." QUIRKS=1 fi # Last safeguard: Try to reach the detected length, if it fails, trigger # quirks mode if [ $QUIRKS -eq 0 ]; then create_temp_dir if ! probe_video "$1" "${VID[$LEN]}" ; then warn "Detected video length can't be reached. Safe measuring enabled." QUIRKS=1 fi fi if [ $QUIRKS -eq 1 ]; then VID[$LEN]=$(safe_length_measure "$1") if [ -z "${VID[$LEN]}" ]; then error "Couldn't measure length in a reasonable amount of tries." if [ $INTERNAL_MAXREWIND_REACHED -eq 1 ]; then error " Will not be able to capture this file with the current settings." else local reqs=$(( $INTERNAL_WS_C + 1 )) reqp='' [ $reqs -eq 1 ] && reqp=" -WP" || reqp=" -WP$reqs" [ $reqs -ge 3 ] && reqs=" -WS" || { # Third try => Recommend -WS [ $reqs -eq 1 ] && reqs=" -Ws" || reqs=" -Ws$reqs" } assert 'fptest "$QUIRKS_MAX_REWIND" -gt 0' local offby=$(pretty_stamp $QUIRKS_MAX_REWIND) warn " Capturing won't work, video is at least $offby shorter than reported." local dname='ffmpeg' [ $decoder -eq $DEC_MPLAYER ] && dname='mplayer' warn " Does $dname support ${VID[$VCODEC]}?." warn " Try re-running with$reqs$reqp." fi return 1 fi elif [ $QUIRKS -eq -2 ]; then warn "Safe mode disabled." fi # Re-check sanity of the most important values is_float "${VID[$LEN]}" || return $RET_NOLEN is_number "${VID[$W]}" && is_number "${VID[$H]}" || return $RET_NODIM } dump_idinfo() { trace $FUNCNAME $@ [ "$MPLAYER" ] && echo "Mplayer: $MPLAYER" [ "$FFMPEG" ] && echo "FFmpeg: $FFMPEG" [ "$MPLAYER" ] && cat <<-EODUMP =========== Mplayer Identification =========== Length: $(pretty_stamp ${VID_MPLAYER[$LEN]}) Video Codec: ${VID_MPLAYER[$VCODEC]} (${VID_MPLAYER[$VCNAME]}) Dimensions: ${VID_MPLAYER[$W]}x${VID_MPLAYER[$H]} FPS: ${VID_MPLAYER[$FPS]} Aspect: ${VID_MPLAYER[$ASPECT]} Audio Codec: ${VID_MPLAYER[$ACODEC]} (${VID_MPLAYER[$ACNAME]}) Channels: ${VID_MPLAYER[$CHANS]} ============================================== EODUMP local ffl="${VID_FFMPEG[$LEN]}" [ "$ffl" ] && ffl=$(pretty_stamp "$ffl") [ -z "$ffl" ] && [ $DVD_MODE -eq 1 ] && ffl="(unavailable in DVD mode)" [ "$FFMPEG" ] && cat <<-EODUMP =========== FFmpeg Identification =========== Length: $ffl Video Codec: ${VID_FFMPEG[$VCODEC]} (${VID_FFMPEG[$VCNAME]}) Dimensions: ${VID_FFMPEG[$W]}x${VID_FFMPEG[$H]} FPS: ${VID_FFMPEG[$FPS]} Aspect: ${VID_FFMPEG[$ASPECT]} Audio Codec: ${VID_FFMPEG[$ACODEC]} (${VID_FFMPEG[$ACNAME]}) Channels: ${VID_FFMPEG[$CHANS]} ============================================= EODUMP local xar= if [ "${VID[$ASPECT]}" ]; then xar=$(keepdecimals "${VID[$ASPECT]}" 4) [ "$xar" ] && xar=" ($xar)" fi cat <<-EODUMP =========== Combined Identification =========== Length: $(pretty_stamp ${VID[$LEN]}) Video Codec: ${VID[$VCODEC]} (${VID[$VCNAME]}) Dimensions: ${VID[$W]}x${VID[$H]} FPS: ${VID[$FPS]} Aspect: ${VID[$ASPECT]}$xar Audio Codec: ${VID[$ACODEC]} (${VID[$ACNAME]}) Channels: ${VID[$CHANS]} ============================================= EODUMP } # Try to pick some font capable of handling non-latin text set_extended_font() { trace $FUNCNAME $@ # This selection includes japanese fonts local candidates=$(identify -list font | grep 'Font: ' | \ egrep -io '[a-z-]*(kochi|mincho|sazanami|ipafont)[a-z-]*') if [ -z "$candidates" ]; then error "Unable to auto-select filename font, please provide one (see -fullhelp)" return 1 else if [ "$DEBUG" -eq 1 ]; then local list=$(echo "$candidates" | sed 's/^/ >/g') inf "Available non-latin fonts detected:" inf "$list" fi fi # Bias towards the Sazanami family if grep -qi 'sazanami' <<<"$candidates" ; then FONT_MINCHO=$(grep -i 'sazanami' <<<"$candidates" | head -1) else FONT_MINCHO=$(head -1 <<<"$candidates") fi } # Checks if the provided arguments make sense and are allowed to be used # together coherence_check() { trace $FUNCNAME $@ # 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)" return $EX_USAGE fi if [ $decoder -eq $DEC_MPLAYER ] && [ -z "$MPLAYER" ]; then inf "No mplayer available. Using ffmpeg only." decoder=$DEC_FFMPEG elif [ $decoder -eq $DEC_FFMPEG ] && [ -z "$FFMPEG" ]; then inf "No ffmpeg available. Using mplayer only." decoder=$DEC_MPLAYER fi if [ $DVD_MODE -eq 1 ] ; then # Currently it's not allowed to use dvd mode with more than one input # "file" (in this mode, input files are actually dvd titles of the file # provided in -V) if [ $multiple_input_files -eq 1 ]; then error "Only an input file is allowed in DVD mode" return $EX_UNAVAILABLE fi # DVD Mode only works with mplayer, the decoder is changed when # the DVD mode option is found, so if it's ffmpeg at this point, # it's by user request (i.e. -F after -V) if [ $decoder -ne $DEC_MPLAYER ]; then if [ "$MPLAYER" ]; then warn "DVD mode requires the use of mplayer, falling back to it" decoder=$DEC_MPLAYER else error "DVD mode requires the use of mplayer." return $EX_UNAVAILABLE fi fi fi local filter= if [ $DISABLE_TIMESTAMPS -eq 0 ] && local -a filts=( ) has_filter filt_polaroid && has_filter filt_apply_stamp ; then for filter in ${FILTERS_IND[@]} ; do if [ "$filter" == "filt_polaroid" ]; then filts=( "${filts[@]}" "$filter" filt_apply_stamp ) elif [ "$filter" == "filt_apply_stamp" ]; then continue; else filts=( "${filts[@]}" $filter ) fi done FILTERS_IND=( "${filts[@]}" ) unset filts fi # The shoftshadow and randrot filters must be in the correct place # or they will affect the image incorrectly. # Additionally the default filters can be disabled from the command # line (with --disable), they're removed from the filter chain here local -a filts=( ) end_filts=( ) for filter in ${FILTERS_IND[@]} ; do case "$filter" in filt_softshadow) # Note the newer soft shadows code (1.0.99 onwards) behaves slightly # differently. On previous versions disabling shadows only affected # the montage shadow (but e.g. the polaroid mode preserved them), # this is no longer true if [ $DISABLE_SHADOWS -ne 1 ]; then end_filts[100]="filt_softshadow" fi ;; filt_apply_stamp) if [ $DISABLE_TIMESTAMPS -ne 1 ]; then filts=( "${filts[@]}" "$filter" ) fi ;; filt_randrot) end_filts[200]="filt_randrot" ;; *) filts=( "${filts[@]}" "$filter" ) ;; esac done FILTERS_IND=( "${filts[@]}" "${end_filts[@]}" ) # Override-able options check, in case they were set from overrides instead #+of equivalent command-line options. Will check the actual selected values, #+i.e. fail silently if the overrides aren't effective [ "$USR_th_height" ] && { check_height "$th_height" || exit $? ; } [ "$USR_numcaps" ] && { check_numcaps "$numcaps" || exit $? ; } [ "$USR_interval" ] && { check_interval "$interval" || exit $? ; } # Interval=0 == default interval fptest "$interval" -eq 0 && interval=$DEFAULT_INTERVAL sanitise_rename_pattern } check_height() { # Acceptable height if ! is_number "$1" && ! is_percentage "$1" ; then error "Height must be a (positive) number or a percentage. Got '$1'." return $EX_USAGE fi } check_numcaps() { # Acceptable numcaps if ! is_number "$1" ; then error "Number of captures must be a (positive) a number! Got '$1'." return $EX_USAGE fi if [ $1 -eq 0 ]; then error "Number of captures must be greater than 0! Got '$1'." return $EX_USAGE fi } check_interval() { # Acceptable interval if ! get_interval "$1" >/dev/null ; then error "Incorrect interval format. Got '$1'." return $EX_USAGE fi } # Main function. # Creates the contact sheet. # process($1 = file) process() { trace $FUNCNAME $@ local f=$1 local numcols= # Save variables that will be overwritten and must be reset with multiple files # pre_* will contain the user-supplied or default values local pre_quirks=$QUIRKS local pre_aspect_ratio=$aspect_ratio local pre_output_format="$output_format" INTERNAL_MAXREWIND_REACHED=0 # Reset for each file # XXX: Some of this should be moved to coherence_check if [ $DVD_MODE -eq 1 ]; then # DVD Mode f="$DVD_FILE" DVD_DEVICE= local dvdn=$(realpathr "$f") # dvdn might be a device or an ISO if [ -f "$dvdn" ]; then # It's an ISO DVD_MOUNTP=$(get_dvd_image_mountpoint) if [ -z "$DVD_MOUNTP" ]; then # Only in Linux does this matter if ! is_linux ; then warn "Video properties detection for ISO files is not accurate" else warn "Mount DVD image to get better video properties detection" fi fi elif [ ! -r "$dvdn" ]; then # It's something else we cannot read error "Can't access DVD ($f)" return $EX_NOINPUT else # It's a device. Note BSD has no concept of block devices. # It MUST be mounted to continue. This is required to allow ffmpeg detection #+and to calculate file size if ! mount | egrep -q "^$dvdn\ " ; then error "DVD mode requires device ($f) to be mounted" return $EX_UNAVAILABLE fi DVD_DEVICE="$dvdn" DVD_MOUNTP=$(mount | grep -o "^$DVD_DEVICE *on [^ ]*" | cut -d' ' -f3) dvdn="DVD" fi inf "Processing $dvdn..." unset dvdn if ! is_number "$1" ; then error "DVD Title must be a number (e.g.: \$ vcs -V /dev/dvd 1)" exit $EX_USAGE fi DVD_TITLE=$1 if [ $DVD_TITLE -eq 0 ]; then local dt="$(lsdvd "$DVD_FILE" 2>/dev/null | grep 'Longest track:' | \ cut -d' ' -f3- | sed 's/^0*//')" if ! is_number "$dt" ; then error "Failed to autodetect longest DVD title" exit $EX_INTERNAL fi DVD_TITLE=$dt DVD_VTS=$(lsdvd -t$DVD_TITLE -v "$DVD_FILE" 2>/dev/null | grep -o 'VTS: [0-9]*' | cut -d' ' -f2) unset dt inf "Using DVD Title #$DVD_TITLE (VTS: $DVD_VTS)" fi else # Not DVD Mode: if [ ! -f "$f" ]; then error "File \"$f\" doesn't exist" return $EX_NOINPUT fi inf "Processing $f..." fi # {{SET_E}} Beware, set -e will break this identify_video "$f" local ecode=$? [ $ecode -eq 0 ] || { case $ecode in 3) error "Unable to find length of file \"$f\". Can't continue." ;; 4) error "Unable to detect dimensions of file \"$f\". Can't continue." ;; *) error "Failure while analysing file \"$f\". Can't continue." ;; esac return $EX_UNAVAILABLE } # Identification-only mode? [ "$UNDFLAG_IDONLY" ] && dump_idinfo && return 0 # Vidcap/Thumbnail height local vidcap_height=$th_height if is_percentage "$th_height" ; then local pc=${th_height/%%/} # BASH %% == RE %$ vidcap_height=$(awkex "int ((${VID[$H]} * ${pc}) / 100 + 0.5)") inf "Height: $th_height of ${VID[$H]} = $vidcap_height" fi if ! is_number "$vidcap_height" || [ "$vidcap_height" -eq 0 ]; then vidcap_height=${VID[$H]} fi # -2: DVD Mode autodetection => If ffmpeg/mplayer was unable autodetect, otherwise #+ honor detected value [ "-2" == "$aspect_ratio" ] && [ -z "${VID[$ASPECT]}" ] && aspect_ratio=-1 [ "-2" == "$aspect_ratio" ] && [ "${VID[$ASPECT]}" ] && aspect_ratio=0 if [ "0" == "$aspect_ratio" ]; then if [ "${VID[$ASPECT]}" ]; then # Aspect ratio in file headers, honor it aspect_ratio=$(awkex "${VID[$ASPECT]}") else aspect_ratio=$(awkex "${VID[$W]} / ${VID[$H]}") fi elif [ "-1" == "$aspect_ratio" ]; then aspect_ratio=$(guess_aspect ${VID[$W]} ${VID[$H]}) inf "Aspect ratio set to $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)... if [ $manual_mode -eq 1 ]; then # Note TIMECODES must be set as an array to get the correct count in # manual mode; in automatic mode it will be set correctly inside # compute_timecodes() TIMECODES=( ${initial_stamps[@]} ) else TIMECODES=${initial_stamps[@]} compute_timecodes $timecode_from $interval $numcaps || { return $? } fi local output=$(new_temp_file '-preview.png') local VIDCAPFILE=00000005.png # If the temporal vidcap already exists, abort if [ -f $VIDCAPFILE ]; then error "File $VIDCAPFILE 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=( "${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 $CTX_HL $n || { local r=$? error "Failed to apply transformations to the capture." return $r } hlcapfile=$(new_temp_file "-hl-$(pad 6 $n).png") mvq "$VIDCAPFILE" "$hlcapfile" capfiles=( "${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 $CTX_STD $n || return $? # identified by capture number, padded to 6 characters capfile=$(new_temp_file "-cap-$(pad 6 $n).png") mvq "$VIDCAPFILE" "$capfile" capfiles=( "${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 $CTX_EXT $n || return $? capfile=$(new_temp_file "-excap-$(pad 6 $n).png") mvq "$VIDCAPFILE" "$capfile" capfiles=( "${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 local vcodec=${VID[$VCNAME]} local acodec=${VID[$ACNAME]} if [ "${VID[$CHANS]}" ] && is_number "${VID[$CHANS]}" &&[ ${VID[$CHANS]} -ne 2 ]; then if [ ${VID[$CHANS]} -eq 1 ]; then acodec="$acodec (mono)" else acodec="$acodec (${VID[$CHANS]}ch)" fi fi local csw=$(imw "$output") exw= hlw= local width=$csw if [ "$HLTIMECODES" ] || [ "$extended_factor" != "0" ]; then inf "Merging contact sheets..." if [ "$HLTIMECODES" ]; then local hlw=$(imw "$hlfile") if [ $hlw -gt $width ]; then width=$hlw ; fi fi if [ "$extended_factor" != "0" ]; then local exw=$(imw $extoutput) if [ $exw -gt $width ]; then width=$exw ; fi fi fi if [ $csw -lt $width ]; then local csh=$(imh "$output") # Expand the standard set to the maximum width of the sets by padding both sides # For some reason the more obvious (to me) convert command-lines lose # the transparency convert \( -size $(( ($width - $csw) / 2 ))x$csh xc:transparent \) "$output" \ \( -size $(( ($width - $csw) / 2 ))x$csh xc:transparent \) +append "$output" unset csh fi # If there were highlights then mix them in if [ "$HLTIMECODES" ]; then # For some reason adding the background also adds padding with: # convert \( -background LightGoldenRod "$hlfile" -flatten \) \ # \( "$output" \) -append "$output" # replacing it with a "-composite" operation apparently works # Expand the highlights to the correct size by padding local hlh=$(imh "$hlfile") if [ $hlw -lt $width ]; then convert \( -size $(( ($width - $hlw) / 2 ))x$hlh xc:transparent \) "$hlfile" \ \( -size $(( ($width - $hlw) / 2 ))x$hlh xc:transparent \) +append "$hlfile" fi convert \( -size ${width}x${hlh} xc:LightGoldenRod "$hlfile" -composite \) \ \( -size ${width}x1 xc:black \) \ "$output" -append "$output" unset hlh fi # Extended captures if [ "$extended_factor" != 0 ]; then # Already set local exw=$(imw "$extoutput") local exh=$(imh "$extoutput") if [ $exw -lt $width ]; then # Expand the extended set to be the correct size convert \( -size $(( ($width - $exw) / 2 ))x$exh xc:transparent \) "$extoutput" \ \( -size $(( ($width - $exw) / 2 ))x$exh xc:transparent \) +append "$extoutput" fi convert "$output" -background Transparent "$extoutput" -append "$output" fi # Add the background; -trim added in 1.11. I'm unsure of why but whithout trimmin extra blank #+space is added at the top local dotrim= [ $DISABLE_SHADOWS -eq 1 ] && [ -z "$HLTIMECODES" ] && dotrim=-trim convert -background "$bg_contact" "$output" -flatten $dotrim "$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=$(imw "$output") # 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. local filename_label="Filename" local filesize_label="File size" local filename_value= local filesize_value= if [ $DVD_MODE -eq 1 ]; then # lsdvd is guaranteed to be installed if DVD mode is enabled local dvd_label=$(lsdvd "$DVD_FILE" 2>/dev/null | grep -o 'Disc Title: .*' | cut -d' ' -f3-) # Need a mountpoint to get the actual *title* size if [ "$DVD_MOUNTP" ]; then filename_label="Disc label" filename_value="$dvd_label" filesize_label="Title size" filesize_value="$(get_dvd_size)" else # Not mounted. We can get the disc size but this will include any other titles. # Since 1.11 mounting DVDs is mandatory to get the title size. Both for ISOs and #+ devices filename_value="$(basename "$DVD_FILE") $filename_value (DVD Label: $dvd_label)" is_linux && warn "DVD not mounted: Can't detect title file size." filesize_label='Disc image size' filesize_value="$(get_pretty_size $(dur "$f"))" fi else filename_value="$(basename "$f")" filesize_value="$(get_pretty_file_size "$f")" fi 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_label: " \ -font "$fn_font" label:"$filename_value" +append \ \) \ -font "$font_heading" \ label:"$filesize_label: $filesize_value" \ 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 local wanted_name=${OUTPUT_FILES[$FILEIDX]} [ "$wanted_name" ] && \ if egrep -q '\.[^\.]+$' <<<"$wanted_name" ; then output_format=$(filext "$wanted_name") inf "Output format set from output filename" else # No file extension in wanted_name wanted_name="$wanted_name.$output_format" fi [ "$wanted_name" ] || wanted_name="$(basename "$f").$output_format" 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" "$wanted_name" ) || { error "Failed to write the output file!" return $EX_CANTCREAT } inf "Done. Output wrote to $output_name" let 'FILEIDX++,1' #,1 so that it's always ok [ "$UNDFLAG_HANG" ] && read -p 'Main loop paused, hit Enter key to continue... ' cleanup # Re-set variables (for multi-file input) QUIRKS=$pre_quirks aspect_ratio=$pre_aspect_ratio output_format="$pre_output_format" } # }}} # Core functionality # {{{ # Debugging helpers # Tests integrity of some operations. # Used to test internal changes for consistency. # It helps me to identify incorrect optimizations. # internal_integrity_test(). Running with -D triggers this. internal_integrity_test() { local t op val ret comm retval=0 # Replacements local SEQ=$(type -pf seq) local JOT=$(type -pf jot) local ex rex if [ "$SEQ" ]; then ex=$($SEQ 1 10) elif [ "$JOT" ]; then ex=$($JOT 10 1) else warn "Can't check seqr() correctness, neither seq nor jot found" fi if [ "$ex" ]; then exr=$(seqr 1 10) if [ "$exr" != "$ex" ]; then error "Failed test: seqr() not consistent with external result" let 'retval++,1' else inf "Passed test (seq replacement): consistent result" fi fi # Textual tests, compare output to expected output # Tests are in the form "operation arguments correct_result #Description" TESTS=( # Note bash2 doesn't like this array as a local variable # TODO: UNIX vs GNU #"stonl ..." "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" #bc result: "pyth_th 16 9 18.35755975068581929849 #FP pythagorean theorem" #perl result: "pyth_th 16 9 18.3575597506858 #FP pythagorean theorem" "pyth_th 16 9 18.3576 #FP pythagorean theorem" "get_interval 2h 7200 #Hours parsing" "get_interval 2m 120 #Minutes parsing" "get_interval 30S 30 #Seconds parsing" "get_interval .30 .30 #Milliseconds parsing" # Since now the numbers are passed to perl, leading zeroes become octal # numbers. Must ensure they are handled correctly "get_interval 09h010m09s1 33010 #Parsing with leading zeroes" "get_interval 0400 400 #Parsing shorthand" # Extended syntax "get_interval 30m30m1h 7200 #Repeated minutes parsing" ) 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=$(grep -o "[^ ]* #$comm\$"<<<$t | cut -d' ' -f1) 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 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=$(grep -o "[^ ]* #$comm\$"<<<$t | cut -d' ' -f1) 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 local inff=inf [ "$HAS_COLORS" ] && inff=infplain $inff "Video Contact Sheet *NIX v${VERSION}, (c) 2007-2010 Toni Corvera" "sgr0" } # Prints the list of options to stdout # show_help($1 = long = '') show_help() { local P=$(basename $0) local showlong=$1 local mpchosen= ffchosen= longhelp= funkyex= [ -z "$MPLAYER" ] && mpchosen=' [Not available]' [ "$MPLAYER" ] && [ $decoder == $DEC_MPLAYER ] && mpchosen=' [Selected]' [ -z "$FFMPEG" ] && ffchosen=', Not available' [ "$FFMPEG" ] && [ $decoder == $DEC_FFMPEG ] && ffchosen=', Selected' # This portion of help is only shown when in full help mode (--fullhelp) [ "$showlong" ] && longhelp=\ " --anonymous Disable the 'Preview created by' line in the footer. -Ij|-Ik|-Ij=fontname|-Ik=fontname --nonlatin Use an alternate font in the heading for the video file name. Required to display correctly file names in some languages (Chinese, Japanese, Hangul, Cyrillic, ...). Will try to use a reasonable font. Can also be set manually like: $ vcs -Ij=Sazanami-Mincho-Regular file.avi or $ vcs -Ij=/usr/share/fonts/ttf/ttf-japanese-mincho.ttf Use \"identify -list font\" to list the available fonts -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\"-). Tweaks and workarounds: -Ws Increase length of safe measuring (try harder). Repeat to increase further. -WS Scan all video, if required, to get a safe measuring. -Wp Increase safe measuring precission (i.e. halve the probe stepping). Repeat to increase further. -WP Inverse of -Wp. -Wo Change ffmpeg's arguments order, might work with some files that fail otherwise. -Wc Disable colour in console messages. Obscure options, debugging tools and workarounds: -R --randomsource Use the provided file as a source for random \"values\": they won't be random anymore, so two runs with the same source and same arguments will produce the same output in modes which use using randomisation (e.g. the \"photos\" and \"polaroid\" modes). -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. " # The --funky help is really long, so make it shorter by default, # only show the complete help when --fullhelp is used [ "$showlong" ] && funkyex=" 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. \"polaroidframe\": Use '-kL' or '--funky polaroidframe' Adds a polaroid picture-like white frame to each image. \"photos\": Use '-kc' or '--funky photos' Combination of rotate, photoframe and overlap. Same as -kp -kr -ko. \"polaroid\": Use '-kp' or '--funky polaroid' Combination of rotate, polaroidframe and overlap. Same as -kL -kr -ko. \"film\": Use '-ki' or '--funky film' Imitates filmstrip look. \"random\": Use '-kx' or '--funky random' Randomizes colours and fonts." [ -z "$showlong" ] && funkyex=" Available: overlap, rotate, photoframe, polaroidframe, photos, polaroid, film, random Use --fullhelp for more details." 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. -o|--output File name of output. When ommited will be derived from the input filename. Can be repeated for multiple files. -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. -T|--title Add a title above the vidcaps. -j|--jpeg Output in jpeg (by default output is in png). -j2|--jpeg 2 Output in jpeg 2000 -V|--dvd DVD Mode, use file.iso as DVD. In this mode the argument must point to the title number, e.g.: $ vcs -V somedvd.iso 1 Passing title 0 will use the default (longest) title. $ vcs -V /dev/dvd 0 Implies -A (auto aspect ratio) -M|--mplayer Use Mplayer to capture$mpchosen -F|--ffmpeg Use FFmpeg to capture [Default$ffchosen] -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. -q|--quiet Don't print progess messages just errors. Repeat to mute completely even on error. -h|--help Show basic help and exit. --fullhelp Show the complete help and exit. -d|--disable Disable some default functionality. Features that can be disabled are: * timestamps: use -dt or --disable timestamps * shadows: use -ds or --disable shadows * padding: use -dp or --disable padding (note shadows introduce some extra padding) -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. -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. -k --funky Funky modes:$funkyex $longhelp Examples: Create a contact sheet with default values (vidcaps at intervals of $DEFAULT_INTERVAL seconds), will be saved to 'video.avi.png': \$ $P video.avi Create a sheet with vidcaps at intervals of 3 and a half minutes, save to 'output.jpg': \$ $P -i 3m30 input.wmv -o output.jpg 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 #### Entry point #### # Important to do this before any message can be thrown init_feedback # Ensure $GETOPT is GNU/Linux-style getopt choose_getopt # Execute exithdlr on exit trap exithdlr EXIT show_vcs_info # Test requirements. Important, must check before looking at the # command line (since getopt is used for the task) test_programs # The command-line overrides any configuration. And the configuration # is able to change the program in charge of parsing options ($GETOPT) 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]] # TODO: Why does FreeBSD's GNU getopt ignore -n?? TEMP=$("$GETOPT" -n "$0" -s bash \ -o i:n:u:T:f:t:S:j::hFMH:c:ma:l:De::U::qAO:I:k:W:E:d:V:R:Z:o:P: \ --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:,dvd:,randomsource:,undocumented:,output:,fullhelp,profile:,"\ "jpeg2,nonlatin" \ -- "$@") eval set -- "$TEMP" while true ; do case "$1" in -i|--interval) check_interval "$2" interval=$(get_interval "$2") timecode_from=$TC_INTERVAL USR_interval=$interval USR_timecode_from=$TC_INTERVAL shift # Option arg ;; -n|--numcaps) check_numcaps "$2" numcaps="$2" timecode_from=$TC_NUMCAPS USR_numcaps="$2" USR_timecode_from=$TC_NUMCAPS shift # Option arg ;; -o|--output) current=${#OUTPUT_FILES[@]} OUTPUT_FILES[$current]="$2" shift ;; -u|--username) user="$2" ; USR_user="$user" ; shift ;; -U|--fullname) # -U accepts an optional 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" ; USR_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 USR_fromtime="$fromtime" 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 USR_end_offset="$end_offset" shift ;; -t|--to) if ! totime=$(get_interval "$2") ; then error "Ending timestamp must be a valid timecode. Got '$2'." exit $EX_USAGE fi if fptest "$totime" -eq 0 ; then error "Ending timestamp was set to 0, set to movie length." totime=-1 fi USR_totime=$totime 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=( "${HLTIMECODES[@]}" "$temp" ) shift ;; --jpeg2) # Note --jpeg 2 is also accepted output_format=jp2 USR_output_format=jp2 ;; -j|--jpeg) if [ "$2" ]; then # Arg is optional, 2 is for JPEG 2000 # 2000 is also accepted if [ "$2" != "2" ] && [ "$2" != "2000" ]; then error "Use -j for jpeg output or -j2 for JPEG 2000 output. Got '-j$2'." exit $EX_USAGE fi output_format=jp2 else output_format=jpg fi USR_output_format="$output_format" shift ;; -h|--help) show_help ; exit $EX_OK ;; --fullhelp) show_help 'full' ; exit $EX_OK ;; --shoehorn) warn "$1 is deprecated, please use '--undocumented shoehorn=\"$2\"' instead" shoehorned="$2" shift ;; -F) decoder=$DEC_FFMPEG ; USR_decoder=$decoder ;; -M) decoder=$DEC_MPLAYER ; USR_decoder=$decoder ;; -H|--height) check_height "$2" th_height="$2" USR_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" USR_aspect_ratio="$2" shift ;; -A|--autoaspect) aspect_ratio=-1 ; USR_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" USR_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 USR_extended_factor=$extended_factor shift ;; --mincho) warn "--mincho is deprecated, please use -Ij or --nonlatin instead" if [ -z "$USR_FONT_MINCHO" ]; then set_extended_font inf "Filename font set to '$FONT_MINCHO'" fi ;; # Unlike -I, --nonlatin does not accept a font name --nonlatin) if [ -z "$USR_FONT_MINCHO" ]; then set_extended_font inf "Filename font set to '$FONT_MINCHO'" fi ;; -I) # Extended/non-latin font # New syntax introduced in 1.11: # -Ij: Try to pick automatically a CJK font. Might fail and abort # -Ij='Font name or file': Set font manually if [ "$2" ] ; then # If an argument is passed, test it is one of the known ones case "$2" in k|j|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; if [ ${#2} -gt 1 ]; then # j=, k= syntax FONT_MINCHO="${2:2}" USR_FONT_MINCHO="$FONT_MINCHO" inf "Filename font set to '$FONT_MINCHO'" fi # If the user didn't pick one, try to select automatically if [ -z "$USR_FONT_MINCHO" ]; then set_extended_font inf "Filename font set to '$FONT_MINCHO'" fi 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 if grep -q 'GETOPT=' <<<"$2" ; then # If we're here, getopt has already been found and works, so it makes no # sense to override it; on the other hand, if it hasn't been correctly # set/detected we won't reach here warn "GETOPT can't be overridden from the command line." else override "$2" "command line" fi shift ;; -W) case "$2" in # (classic) Workaround mode. See wa_ss_* declarations at the start for details o) wa_ss_af='-ss ' ; wa_ss_be='' ;; # Console colout # Once: Disable console colour, use prefixes instead # Twice: Disable prefixes too c) set_feedback_prefixes [ "$UNDFLAG_NOPREFIX" ] && plain_messages=1 UNDFLAG_NOPREFIX=1 ;; # Double length of video probed in safe measuring # Semi-undocumented traits: # - Can be repeated, will double for each instance # - -Ws -Ws -Ws = -Ws3 s|s[0-9]|s[0-9][0-9]) [ ${#2} -gt 1 ] && n=${2:1} || n=1 QUIRKS_MAX_REWIND=$(awkex "$QUIRKS_MAX_REWIND * (2^$n)") let 'INTERNAL_WS_C+=n,1' ;; # Brute force -Ws: Test all the length of the file if required S) QUIRKS_MAX_REWIND=-1 ;; # Increase precission of safe length measuring (halve the stepping) # Like -Ws can be repeated p|p[0-9]|p[0-9][0-9]) [ ${#2} -gt 1 ] && n=${2:1} || n=1 QUIRKS_LEN_STEP=$(awkex "$QUIRKS_LEN_STEP / (2^$n)") let 'INTERNAL_WP_C+=n,1' ;; # Inverse of -Wp: Decrease precission of safe length measuring # i.e.: will try less times <-> will be quicker but less accurate # desirable when -Ws or -WS are used. # Can also be repeated P|P[0-9]|P[0-9][0-9]) [ ${#2} -gt 1 ] && n=${2:1} || n=1 QUIRKS_LEN_STEP=$(awkex "$QUIRKS_LEN_STEP * (2^$n)") let 'INTERNAL_WP_C-=n,1' ;; # -Wb (Semi-undocumented): Disable safe mode. Use this to force accepting #+broken/partial files. Only makes sense when testing or in combination #+with stuff like '-Z idonly' b) QUIRKS=-2 ;; # Quirks < 0 : No safe mode *) error "Wrong argument. Use --fullhelp for a list available workarounds. Got -W$2." exit $EX_USAGE ;; esac shift ;; -k|--funky) # Funky modes case "$2" in # Note older versions (<1.0.99) were case-insensitive p|polaroid) # Same as overlap + rotate + polaroid inf "Changed to polaroid funky mode." FILTERS_IND=( "${FILTERS_IND[@]}" 'filt_polaroid' 'filt_randrot' ) CSHEET_DELEGATE='csheet_overlap' # XXX: The newer version has a lot less flexibility with these many # hardcoded values... grav_timestamp=South fg_tstamps=Black bg_tstamps=Transparent pts_tstamps=$(( $pts_tstamps * 3 / 2 )) ;; c|photos) # Same as overlap + rotate + photoframe, this is the older polaroid inf "Changed to photos funky mode." FILTERS_IND=( "${FILTERS_IND[@]}" 'filt_photoframe' 'filt_randrot' ) CSHEET_DELEGATE='csheet_overlap' # The timestamp must change location to be visible most of the time grav_timestamp=NorthWest ;; o|overlap) # Random overlap mode CSHEET_DELEGATE='csheet_overlap' grav_timestamp=NorthWest ;; r|rotate) # Random rotation FILTERS_IND=( "${FILTERS_IND[@]}" 'filt_randrot' ) ;; f|photoframe) # White photo frame FILTERS_IND=( "${FILTERS_IND[@]}" 'filt_photoframe' ) ;; L|polaroidframe) # White polaroid frame FILTERS_IND=( "${FILTERS_IND[@]}" 'filt_polaroid ') grav_timestamp=South fg_tstamps=Black bg_tstamps=Transparent pts_tstamps=$(( $pts_tstamps * 3 / 2 )) ;; i|film) inf "Enabled film mode." FILTERS_IND=( "${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 ;; -P|--profile) case "$2" in classic) # Classic colour scheme bg_heading=YellowGreen bg_sign=SlateGray bg_contact=White bg_title=White fg_heading=Black fg_sign=Black ;; 1.0) # 1.0a, 1.0.1a and 1.0.2b colourscheme bg_heading=YellowGreen bg_sign=SandyBrown bg_contact=White bg_title=White fg_heading=Black fg_sign=Black ;; esac shift ;; -R|--randomsource) if [ ! -r "$2" ]; then error "Random source file '$2' can't be read" exit $EX_USAGE fi init_filerand "$2" inf "Using '$2' as source of semi-random values" RANDFUNCTION=filerand shift ;; -d|--disable) # Disable default features case $(tolower "$2") in # timestamp (with no final s) is undocumented but will stay t|timestamps|timestamp) if [ $DISABLE_TIMESTAMPS -eq 0 ]; then inf "Timestamps disabled." # They'll be removed from the filter chain in coherence_check DISABLE_TIMESTAMPS=1 fi ;; s|shadows|shadow) if [ $DISABLE_SHADOWS -eq 0 ]; then inf "Shadows disabled." # They will be removed from the filter chain in coherence_check DISABLE_SHADOWS=1 fi ;; p|padding) if [ $HPAD -ne 0 ] ; then inf "Padding disabled." # Kinda... HPAD=0 fi ;; *) error "Requested disabling unknown feature. Got '$2'." exit $EX_USAGE ;; esac shift ;; -V|--dvd) # XXX; Are there systems with no perl??? if ! type -pf perl >/dev/null ; then error "DVD support requires perl" exit $EX_UNAVAILABLE fi # DVD Mode requires lsdvd if ! type -pf lsdvd >/dev/null ; then error "DVD support requires the lsdvd program" exit $EX_UNAVAILABLE fi DVD_MODE=1 DVD_FILE="$2" decoder=$DEC_MPLAYER aspect_ratio=-2 # Special value: Auto detect only if ffmpeg couldn't 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 ;; -Z|--undocumented) # This is a container for, of course, undocumented functions # These are used for testing/debugging purposes. Might (and will) # change between versions, break easily and do no safety checks. # In short, don't look at them unless told to do so :P case "$2" in # AWK was used for a little while in a WiP version #set_awk=*) AWK="$(cut -d'=' -f2<<<"$2")" ; warn "[U] AWK=$AWK" ;; # Hang the main process loop just before cleanup. hang) UNDFLAG_HANG="On" ; warn "[U] Hang flag" ;; # Print identification results, do nothing else idonly) UNDFLAG_IDONLY="On" ; warn "[U] Id only" ;; # ffmpeg path set_ffmpeg=*) FFMPEG=$(realpathr "$(cut -d'=' -f2<<<"$2")") assert $LINENO 'test -x "$FFMPEG"' warn "[U] FFMPEG=$FFMPEG" ;; # mplayer path set_mplayer=*) MPLAYER=$(realpathr "$(cut -d'=' -f2<<<"$2")") assert $LINENO 'test -x "$MPLAYER"' warn "[U] MPLAYER=$MPLAYER" ;; # Ignore one of the players disable_ffmpeg) FFMPEG='' warn "FFmpeg disabled" assert $LINENO "[ '$MPLAYER' ] || [ '$FFMPEG' ]" decoder=$DEC_MPLAYER ;; disable_mplayer) MPLAYER='' warn "Mplayer disabled" assert $LINENO "[ '$MPLAYER' ] || [ '$FFMPEG' ]" decoder=$DEC_FFMPEG ;; # This is an old option from the first versions when the script # failed a lot more, I haven't used it for years and I don't think # anyone would need it anymore but I'll keep it at least for # a few more versions shoehorn=*) shoehorned="$(cut -d'=' -f2-<<<"$2")" ;; *) false ;; esac shift ;; -D) # Repeat to just test consistency if [ $DEBUGGED -gt 0 ]; then [ $decoder -eq $DEC_MPLAYER ] && d='mplayer' [ $decoder -eq $DEC_FFMPEG ] && d='ffmpeg' infplain '[ svn $Rev$ ]' cat >&2 <<-EOD === Setup === GETOPT: $GETOPT MPLAYER: $MPLAYER FFMPEG: $FFMPEG AWK: $(type -pf awk) Filterchain: [ ${FILTERS_IND[*]} ] Decoder: $d Safe step: $QUIRKS_LEN_STEP === Versions === Bash: $BASH_VERSION Getopt: $($GETOPT --version) EOD # FIXME: Any portable way to print AWK version? exit fi DEBUG=1 inf "Testing internal consistency..." internal_integrity_test && warn "All tests passed" || error "Some tests failed!" 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 elif [ "$2" ]; then multiple_input_files=1 fi # }}} # Command line parsing # The coherence check ensures the processed options are # not incoherent/incompatible with the input files or with # other given options coherence_check || { exit $? } set +e # Don't fail automatically. Blocks marked with {{SET_E}} will break if this changes for arg do process "$arg" ; done # Script ends here, everything below are comments # =========================================================================== # # Bash syntax notes # {{{ # These are some notes for my own reference (or for those trying to read the script) # regarding bash syntax nuissances. # # * herestring redirection, '<<<$string', (used extensively in vcs) was introduced in bash 2.05b # * sed s/[ ,]/ * /g <=> ${var//[ ,]/ * } [Much faster due to not forking] # sed s/[ ,]/ * / <=> ${var/[ ,]/ * } # * bash2: declaring local empty arrays like 'local a=( )' makes bash think they're strings # 'local -a' must be used instead # bash3 has no problem with this # * bash2: 'arr+=( elem )' for array push is not supported, use 'arr=( "${arr[@]}" elem )' instead # += is a bash3 syntax modification, bash3.1 extended it further, arithmetic += works # inside let # * bash2: [*] expands as a string while [@] expands as an array. Both have trouble with spaces # in elements though # * performance: bash loops are often slower than awk or perl # * performance: grep + cut proved faster than an equivalent sed -r s// replacement # }}} # Bash syntax notes # # vim:set ts=4 ai foldmethod=marker nu: #