#!/bin/sh # SPDX-License-Identifier: CC0-1.0 # VideoStore Codec by Digital Assistant # 56 years after UNIX epoch # v0.0.3 # Single-hash comments written or verified by human. ## Double-hash comments (other than this one) written by bot, not verified. # More info - # Direct nostr link: View quoted note → # Legacy web link: # Internet Archive legacy web backup: set -e VERSION=$(sed -n '6s/^# //p' "$0" 2>/dev/null || echo "unknown") ## --- Localization Setup --- export TEXTDOMAIN="videostore-codec" export TEXTDOMAINDIR="${TEXTDOMAINDIR:-/usr/share/locale}" ## Fallback function if gettext is not installed on the system if ! command -v gettext >/dev/null 2>&1; then gettext() { printf "%s" "$1" } fi ## Ensure consistent numeric and sorting behavior across POSIX shells export LC_NUMERIC=C export LC_COLLATE=C START_TIME=$(date +%s 2>/dev/null) || { echo "$(gettext "Warning: Process timer maybe unavailable on this system.")"; START_TIME=""; } usage() { exit_code="${1:-1}" ## If exiting with an error, redirect help text to stderr to avoid pipe corruption [ "$exit_code" -ne 0 ] && exec 1>&2 printf "$(gettext "VideoStore Codec %s\n")" "$VERSION" echo "$(gettext "Usage:")" echo "$(gettext " Encoding Mode (Directory -> Video/Images):")" printf "$(gettext " %s -e <input_dir> <output_dir> [options]\n")" "$0" echo "$(gettext " Add par2 recovery files, pack tarball, encode video")" echo "$(gettext " (default filename: <input_dir>.tar.mkv)")" echo "" echo "$(gettext " Decoding Mode (Video -> Restored Tarball):")" printf "$(gettext " %s -d <input_video> <output_dir> [options]\n")" "$0" echo "$(gettext " Should also accept an input directory of still frame images")" echo "" echo "$(gettext "Options:")" echo "$(gettext " -o DIR Use a permanent directory for intermediate files (bypasses mktemp)")" echo "$(gettext " -h Show this help message")" echo "$(gettext "Decoder only:")" echo "$(gettext " -b Bin-only mode: stop at .bin extraction if input is video,")" echo "$(gettext " start reassembler from .bin directory if input is directory")" echo "$(gettext " -g NUM Grayscale mode (1=on, 0=off) - If not set, may interrupt to prompt")" echo "$(gettext " (Set 1 unless payload uses color)")" echo "$(gettext "Encoder only:")" echo "$(gettext " -i Output image folders (dm/ and qr/) instead of a video")" echo "$(gettext " -r Raw mode: input a single file, skip tarball+par2")" echo "$(gettext " -l NUM Number of extra loop sets to generate, default 0 (play once)")" echo "$(gettext " (Setting any -l value also should also disable the nag screen)")" echo "$(gettext " -k NUM Include bootstrapping kit (mini script QR codes, full script embed)")" echo "$(gettext " 0=off, 1=on (default)")" echo "$(gettext " -s NUM Set par2 block size in 8-bit bytes")" echo "$(gettext " Default 1500, can handle around 48,000,000 bytes maximum")" echo "$(gettext " INCREASE on large payloads to DECREASE overhead")" echo "$(gettext " -c FG BG Set custom Hex colors for Foreground and Background")" echo "$(gettext " Defaults: 000000 6B6B6B (black and gray)")" echo "$(gettext " Can use 000000 FFFFFF for standard black and white")" echo "$(gettext " -t LEVEL Throughput level (0-15), default 8 (364 bytes/frame)")" echo "$(gettext " Levels: 0=40, 1=58, 2=82, 3=110, 4=140, 5=170, 6=200, 7=276, 8=364,")" echo " 9=452, 10=572, 11=692" echo "" echo "$(gettext " More info -")" echo "$(gettext " Direct nostr link:")" echo "$(gettext " View quoted note →")" echo "$(gettext " Legacy web link:")" echo "$(gettext " https://jumble.social/notes/nevent1qvzqqqqqqypzqamkcvk5k8g730e2j6atadp6mxk7z4aaxc7cnwrlkclx79z4tzygqy88wumn8ghj7mn0wvhxcmmv9uq32amnwvaz7tmjv4kxz7fwv3sk6atn9e5k7tcqyq00jwdmshl99qk993hek9tasux80kuauxn95v6qzgyu3deag7qygget24w")" echo "$(gettext " Internet Archive legacy web backup:")" echo "$(gettext " https://archive.org/details/videostore-codec")" exit "$exit_code" } check_encoder_dependencies() { ## $1: is_video mode (requires ffmpeg) ## $2: is_raw mode (bypasses par2) if ! command -v mktemp >/dev/null 2>&1; then echo "$(gettext "Warning: 'mktemp' not found. Can try again with -o for a permanent work directory.")" >&2 fi if ! command -v zint >/dev/null 2>&1; then echo "$(gettext "Error: can't find tool 'zint'")" >&2 exit 1 fi if [ "$2" != "true" ] && ! command -v par2 >/dev/null 2>&1; then echo "$(gettext "Error: can't find tool 'par2'")" >&2 exit 1 fi if [ "$1" = "true" ] && ! command -v ffmpeg >/dev/null 2>&1; then echo "$(gettext "Error: bro, do you somehow not have ffmpeg?")" >&2 exit 1 fi } check_decoder_dependencies() { ## $1: input_is_dir (skips ffmpeg) ## $2: raw_mode (skips par2) if ! command -v mktemp >/dev/null 2>&1; then echo "$(gettext "Warning: 'mktemp' not found. Can try again with -o for a permanent work directory.")" >&2 fi if ! command -v ZXingReader >/dev/null 2>&1; then echo "$(gettext "Error: can't find tool 'ZXingReader' (from package: zxing-cpp or zxing-cpp-tools)")" >&2 exit 1 fi if [ "$1" != "true" ] && ! command -v ffmpeg >/dev/null 2>&1; then echo "$(gettext "Error: bro, do you somehow not have ffmpeg installed?")" >&2 exit 1 fi if [ "$2" != "true" ] && ! command -v par2 >/dev/null 2>&1; then echo "$(gettext "Error: can't find tool 'par2'")" >&2 exit 1 fi } get_file_md5_full() { infile="$1" if command -v md5sum >/dev/null 2>&1; then md5sum -- "$infile" | awk '{print $1}' elif command -v md5 >/dev/null 2>&1; then ## Try -q (BSD), fallback to awk parsing if -q is unsupported md5 -q -- "$infile" 2>/dev/null || md5 -- "$infile" | awk '{print $NF}' else echo "00000000000000000000000000000000" fi } hex_to_octal_fmt() { printf '%s\n' "$1" | tr -d ' ' | awk ' BEGIN { for (i = 0; i < 256; i++) { h = sprintf("%02x", i); H = sprintf("%02X", i); o = sprintf("\\%03o", i) map[h] = o; map[H] = o } } { for (i = 1; i <= length($0); i += 2) printf "%s", map[substr($0, i, 2)] } ' } EXTRACTOR_SCRIPT=$(cat << 'EOF' #!/bin/sh # VideoStore MiniExtractor 0.0.3 # Nostr info link - View quoted note → # Internet Archive legacy web backup - set -e export TEXTDOMAIN=videostore-codec TEXTDOMAINDIR=${TEXTDOMAINDIR:-/usr/share/locale} LC_NUMERIC=C LC_COLLATE=C ! command -v gettext >/dev/null && gettext() { printf %s "$1"; } # usage info u() { z=${1:-1}; [ $z != 0 ] && exec 1>&2; echo echo "$(gettext "This script goes before reassembler, to prepare files.")" echo "$(gettext "REQUIREMENTS: ffmpeg + zxing-cpp / zxing-cpp-tools")" printf "$(gettext "USAGE: %s <input_vid> <output_dir> [options]\n")" "$0"; echo "$(gettext "OPTIONS:")" echo "$(gettext " -o DIR | Permanent work directory, to bypass mktemp/cleanup")" echo "$(gettext " -g 0/1 | Grayscale mode, unset with 0 if payload requires color")" exit $z } e() { echo; echo "$(gettext "Error (miniscript has no detailed error messages)")"; u; } h2o() { printf %s "$1" | tr -d ' ' | awk 'BEGIN{for(i=0;i<256;i++)m[sprintf("%02x",i)]=m[sprintf("%02X",i)]=sprintf("\\%03o",i)}{for(k=1;k<=length($0);k+=2)printf"%s",m[substr($0,k,2)]}' } i2b() { [ "$4" = 1 ] && p="-pix_fmt gray" || p="" [ -d "$1" ] && s=$1 || { ffmpeg -i "$1" $p "$2/f_%08d.png"; s=$2; } [ "$4" = 1 ] && echo "$(gettext "Grayscale mode conserves I/O, unset with -g 0 if payload requires color")" echo "$(gettext "Attempting extraction, feed output to reassembler when done")" M=$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1); j=0 for i in "$s"/*.png; do [ -f "$i" ] || continue ( D=$(ZXingReader -format DataMatrix -single "$i" 2>/dev/null || :) H=$(printf '%s\n' "$D" | sed -n 's/^Bytes: *//p' | tr -d '\r\n') if [ -n "$H" ]; then t="$3/tmp_$(basename "$i").bin" printf %b "$(h2o "$H")" > "$t" Q=$(ZXingReader -format QRCode -single "$i" 2>/dev/null | sed -n 's/^Text: *"\([^"]*\)".*/\1/p' | tr -d '\r\n' || :) if [ -n "$Q" ]; then mv -- "$t" "$3/$Q.bin" || { touch "$2/.failed"; e; } else rm -f -- "$t"; fi; fi ) & j=$((j+1)); [ $j -ge $M ] && wait && j=0 done wait; [ -f "$2/.failed" ] && e } G="1"; P=""; I=""; O="" while [ $# -gt 0 ]; do case "$1" in -o) if [ -z "$2" ] || [ "$2" != "${2#-}" ]; then e; fi; P="$2"; shift 2 ;; -g) if [ -z "$2" ] || ! echo "$2" | grep -Eq '^[01]$'; then e; fi; G="$2"; shift 2 ;; -*) e ;; *) if [ -z "$I" ]; then I="$1"; elif [ -z "$O" ]; then O="$1"; else e; fi; shift ;; esac done [ -z "$I" ] || [ -z "$O" ] || ! command -v ZXingReader >/dev/null 2>&1 && e [ ! -e "$I" ] && e [ ! -d "$I" ] && ! command -v ffmpeg >/dev/null 2>&1 && e if [ -n "$P" ]; then W="$P"; mkdir -p -- "$W" || e else W=$(mktemp -d); trap 'r=$?; rm -rf "$W"; exit $r' 0 INT TERM HUP; fi mkdir -p -- "$O" || e i2b "$I" "$W" "$O" "$G" EOF ) REASSEMBLER_SCRIPT=$(cat << 'EOF' #!/bin/sh # VideoStore MiniReassembler 0.0.3 # Nostr info link - View quoted note → # Internet Archive legacy web backup - set -e export TEXTDOMAIN=videostore-codec TEXTDOMAINDIR=${TEXTDOMAINDIR:-/usr/share/locale} LC_NUMERIC=C LC_COLLATE=C ! command -v gettext >/dev/null && gettext() { printf %s "$1"; } # usage info u() { z=${1:-1}; [ $z != 0 ] && exec 1>&2; echo echo "$(gettext "This script goes after the extractor, needs it to prepare files.")" echo "$(gettext "Might also need par2 installed for error correction.")" printf "$(gettext "USAGE: %s <bin_dir> <output_dir> [options]\n")" "$0"; echo "$(gettext "OPTIONS:")" echo "$(gettext " -o DIR | Permanent work directory, to bypass mktemp/cleanup")" exit $z } e() { echo; echo "$(gettext "Error (miniscript has no detailed error messages)")"; u; } r2f() { s="$1"; d="$2"; m="$3"; w="$4"; p="$w/p" : > "$p" for f in "$s"/*.bin; do [ -f "$f" ] || continue n=${f##*/}; t=${n#*.}; t=${t%.bin} [ "$t" != "$n" ] && printf '%s\n' "$t" >> "$p" done [ -s "$p" ] || return 0 sort -u "$p" | while read -r t; do [ -z "$t" ] && continue c="$s/00000000.$t.bin" [ -f "$c" ] || continue Z=$(sed -n '3p' "$c" | awk '{print $1}') k=$(sed -n '4p' "$c" | awk '{print $3}') [ -n "$Z" ] && [ -n "$k" ] || continue E=$(( (Z + k - 1) / k )); o="$d/$t"; : > "$o"; b=0 i=1 while [ "$i" -le "$E" ]; do h="$s/$(printf "%08d" "$i").$t.bin" if [ -f "$h" ]; then cat "$h" >> "$o" else b=1; [ "$i" -eq "$E" ] && l=$(( Z - (E - 1) * k )) || l="$k" dd if=/dev/zero bs=1 count="$l" >> "$o" 2>/dev/null fi i=$((i + 1)) done if [ "$m" != "true" ]; then tar -xf "$o" -C "$d" x="$d/${t%.tar}" R=0 if [ "$b" = 1 ]; then if [ -d "$x" ] && [ -f "$x/videostore.par2" ]; then (cd "$x" && par2 repair videostore.par2 >/dev/null 2>&1) && R=1 || R=0 else R=0 fi else R=1 fi [ "$R" -eq 1 ] && rm -f "$o" && [ -d "$x" ] && rm -f "$x"/videostore*.par2 fi done } echo "$(gettext "Running, check output when done (may need par2 installed to work)")" I="" O="" P="" while [ $# -gt 0 ]; do case "$1" in -o) if [ -z "$2" ] || [ "$2" != "${2#-}" ]; then e; fi; P="$2"; shift 2 ;; -*) e ;; *) if [ -z "$I" ]; then I="$1"; elif [ -z "$O" ]; then O="$1"; else e; fi; shift ;; esac done [ -z "$I" ] || [ -z "$O" ] && e if [ -n "$P" ]; then W="$P"; mkdir -p -- "$W" || e else W=$(mktemp -d); trap 'r=$?; rm -rf "$W"; exit $r' 0 INT TERM HUP; fi mkdir -p -- "$O" || e; r2f "$I" "$O" "false" "$W" EOF ) ## --- Core Logic Functions --- encode_dm() { input_source="$1" out_target="$2" fg_color="$3" bg_color="$4" barcode_loops="$5" chunk_size="$6" dm_scale="$7" perm_dir="$8" l_flag_set="$9" images_only="${10}" raw_mode="${11}" par2_size="${12}" gen_scripts="${13}" EXTRA_WRAPPER="" work_dir="" CLEAN_EXIT="false" cleanup_encode() { ## Reset traps to prevent recursion trap - EXIT INT TERM HUP if [ "$CLEAN_EXIT" != "true" ]; then echo "" >&2 echo "$(gettext "EXIT: Attempting to clean up temporary files…")" >&2 fi ## Kill background jobs safely _pids=$(jobs -p) [ -n "$_pids" ] && kill $_pids 2>/dev/null || true [ -n "$EXTRA_WRAPPER" ] && [ -d "$EXTRA_WRAPPER" ] && rm -rf "$EXTRA_WRAPPER" [ -n "$work_dir" ] && [ -d "$work_dir" ] && rm -rf "$work_dir" } if [ "$images_only" = "true" ]; then mode_v="false" else mode_v="true" fi ## Edge case: A file was provided as input, but the user did not specify raw mode (-r) if [ "$raw_mode" = "false" ] && [ -f "$input_source" ]; then echo "=========================================================================" echo "$(gettext " You may have input a single file while raw mode (-r) was not selected.")" echo "$(gettext " Standard mode expects a directory.")" echo "" echo "$(gettext " How would you like to proceed?")" echo "$(gettext " 1) Enable Raw Mode (-r) automatically (no error correction)")" echo "$(gettext " 2) Wrap in Tarball (adds overhead for error correction)")" echo "$(gettext " 3) Cancel Encoding / Start Over")" echo "=========================================================================" printf "$(gettext "Selection [1/2/3]: ")" read -r file_choice case "$file_choice" in 1) raw_mode="true" ;; 2) ## Create a temporary wrapper to satisfy the directory mode requirements EXTRA_WRAPPER=$(mktemp -d) BASE_NAME=$(basename -- "$input_source") mkdir -p -- "$EXTRA_WRAPPER/$BASE_NAME" cp -- "$input_source" "$EXTRA_WRAPPER/$BASE_NAME/" ## Establish cleanup if we've created temp files [ -z "$perm_dir" ] && trap cleanup_encode EXIT INT TERM HUP input_source="$EXTRA_WRAPPER/$BASE_NAME" ;; 0|3|*) echo "$(gettext "Operation canceled.")" exit 1 ;; esac fi check_encoder_dependencies "$mode_v" "$raw_mode" if [ "$raw_mode" = "true" ]; then if [ ! -f "$input_source" ]; then printf "$(gettext "Error: File '%s' not found\n")" "$input_source" >&2 exit 1 fi filename=$(basename -- "$input_source") else if [ ! -d "$input_source" ]; then printf "$(gettext "Error: Directory '%s' not found\n")" "$input_source" >&2 exit 1 fi dir_basename=$(basename -- "$input_source") filename="$dir_basename.tar" fi ## Reject problematic shell characters and leading dashes, but allow Unicode in general case "$filename" in -*) printf "$(gettext "Error: Name '%s' cannot start with a dash.\n")" "$filename" >&2 exit 1 ;; *[[:space:]]*) printf "$(gettext "Error: Name '%s' cannot contain spaces or whitespace.\n")" "$filename" >&2 exit 1 ;; *[\`\$\(\)\<\>\\\|\;\&\*\"\'\?\!]*) printf "$(gettext "Error: Name '%s' contains prohibited shell metacharacters.\n")" "$filename" >&2 echo "$(gettext "Please avoid using characters like: \` $ ( ) < > \\ | ; & * \" ' ? !")" >&2 exit 1 ;; esac input_source_parent=$(dirname -- "$input_source") input_source_abs="$(cd -- "$input_source_parent" && pwd)/$(basename -- "$input_source")" mkdir -p -- "$out_target" out_target_abs="$(cd -- "$out_target" && pwd)" echo "$(gettext "Trying to create work folder…")" if [ -n "$perm_dir" ]; then mkdir -p -- "$perm_dir" work_dir="$(cd -- "$perm_dir" && pwd)" else work_dir=$(mktemp -d) trap cleanup_encode EXIT INT TERM HUP fi orig_pwd=$(pwd) if [ "$raw_mode" = "true" ]; then input_abs_path="$input_source_abs" else if [ -n "$perm_dir" ]; then if [ "$gen_scripts" = "1" ]; then cp -- "$0" "$input_source/$(basename -- "$0")" fi printf "$(gettext "Using permanent mode: Generating par2 for '%s'...\n")" "$input_source" (cd "$input_source" && rm -f videostore*.par2 && par2 create -r30 -n2 -s"$par2_size" videostore * >/dev/null) tar_file="$work_dir/$filename" (cd "$input_source_parent" && tar -cf "$tar_file" "$dir_basename") input_abs_path="$tar_file" else printf "$(gettext "Preparing '%s' with par2 recovery...\n")" "$dir_basename" mkdir -p -- "$work_dir/pack/$dir_basename" cp -r "$input_source/." "$work_dir/pack/$dir_basename/" if [ "$gen_scripts" = "1" ]; then cp -- "$0" "$work_dir/pack/$dir_basename/$(basename -- "$0")" fi ## Generate parity for all files in the directory (cd "$work_dir/pack/$dir_basename" && rm -f videostore*.par2 && par2 create -r30 -n2 -s"$par2_size" videostore * >/dev/null) tar_file="$work_dir/$filename" (cd "$work_dir/pack" && tar -cf "$tar_file" "$dir_basename") input_abs_path="$tar_file" fi fi cd -- "$work_dir" printf "$(gettext "Splitting '%s' into %s-byte chunks (adjust with -t option)...\n")" "$filename" "$chunk_size" mkdir -p chunks split -b "$chunk_size" -- "$input_abs_path" chunks/_ echo "$(gettext "Preparing data matrix sequence...")" file_md5_full=$(get_file_md5_full "$input_abs_path") mkdir -p staging_dm mkdir -p staging_qr ## 1. Generate Metadata Header Matrix header_dm="staging_dm/00000000.png" filesize=$(wc -c < "$input_abs_path" | tr -d ' ') printf "MD5: %s\n%s\n%s bytes\nChunk size: %s\n%s\n%s\nMore info / readme (nostr link) -\nView quoted note →" "$file_md5_full" "$filename" "$filesize" "$chunk_size" "VideoStore codec $VERSION" "file data from next data matrix until metadata code repeats" > header.tmp zint -b 71 --scale="$dm_scale" --whitesp=6 --vwhitesp=6 --fg="$fg_color" --bg="$bg_color" -i header.tmp -o "$header_dm" ## 2. Generate Data Matrix Payload chunk_count=0 for f in chunks/_*; do [ -f "$f" ] || continue chunk_count=$((chunk_count + 1)) seq_num=$(printf "%08d" "$chunk_count") zint -b 71 --binary --scale="$dm_scale" --whitesp=6 --vwhitesp=6 --fg="$fg_color" --bg="$bg_color" -o "staging_dm/${seq_num}.png" -i "./$f" done ## 3. Generate QR codes header_bc_content="00000000.${filename}" zint -b 58 --secure=4 --fg="$fg_color" --bg="$bg_color" --scale=3 --whitesp=6 --vwhitesp=6 -o "staging_qr/00000000.png" -d "$header_bc_content" i=1 while [ "$i" -le "$chunk_count" ]; do seq_num=$(printf "%08d" "$i") barcode_content="${seq_num}.${filename}" zint -b 58 --secure=4 --fg="$fg_color" --bg="$bg_color" --scale=3 --whitesp=6 --vwhitesp=6 -o "staging_qr/${seq_num}.png" -d "$barcode_content" i=$((i + 1)) done ## Buffer for video mode if [ "$mode_v" = "true" ]; then j=1 while [ "$j" -le 10 ]; do extra_seq=$(printf "%08d" "$((chunk_count + j))") cp "staging_qr/00000000.png" "staging_qr/${extra_seq}.png" cp "staging_dm/00000000.png" "staging_dm/${extra_seq}.png" j=$((j + 1)) done fi if [ "$mode_v" = "true" ]; then echo "$(gettext "Compiling side-by-side lossless VP9 video...")" if [ "$l_flag_set" = "false" ]; then echo "=========================================================================" echo "$(gettext " NOTICE: It is recommended to loop the output sequence,")" echo "$(gettext " in your video editor, at least once (playing twice).")" echo "$(gettext " There is a less reliable loop function included in")" echo "$(gettext " this script, not recommended, but available.")" echo "" echo "$(gettext " Would you like to use the less reliable built-in loop?")" echo "$(gettext " Press ENTER to skip / handle later (RECOMMENDED)")" echo "$(gettext " Type a number to set loop count (NOT RECOMMENDED)")" echo "$(gettext " Type 'C' to cancel encode / start over")" echo "$(gettext " (this screen may be skipped from the start with -l 0)")" echo "=========================================================================" read -r user_loops case "$user_loops" in [cC]|[cC][aA][nN][cC][eE][lL]) echo "$(gettext "Operation canceled. Exiting.")" exit 0 ;; esac if [ -n "$user_loops" ] && echo "$user_loops" | grep -Eq '^[0-9]+$'; then barcode_loops="$user_loops" fi fi loop_opts="" if [ "$barcode_loops" -gt 0 ]; then loop_opts="-stream_loop $barcode_loops" fi video_out_abs="$out_target_abs/$filename.mkv" if [ "$raw_mode" = "false" ] && [ "$gen_scripts" = "1" ]; then echo "$(gettext "Trying to generate recovery miniscript QR codes...")" printf "%s" "$EXTRACTOR_SCRIPT" > "$work_dir/extractor.sh" printf "%s" "$REASSEMBLER_SCRIPT" > "$work_dir/reassembler.sh" zint -b 58 --fg="$fg_color" --bg="$bg_color" --scale=2 --whitesp=4 --vwhitesp=4 -i "$work_dir/extractor.sh" -o "$out_target_abs/$filename.extractor.qr.png" zint -b 58 --fg="$fg_color" --bg="$bg_color" --scale=2 --whitesp=4 --vwhitesp=4 -i "$work_dir/reassembler.sh" -o "$out_target_abs/$filename.reassembler.qr.png" fi ffmpeg -reinit_filter 0 $loop_opts -thread_queue_size 16384 -framerate 25 -i staging_dm/%08d.png \ -reinit_filter 0 $loop_opts -thread_queue_size 16384 -framerate 25 -i staging_qr/%08d.png \ -filter_complex "[0:v]pad=540:540:0:540-ih:color=black@0[dm]; \ [1:v]pad=540:540:0:540-ih:color=black@0[qr]; \ [dm][qr]hstack=inputs=2,format=yuva420p[out]" \ -map "[out]" -c:v libvpx-vp9 -lossless 1 "$video_out_abs" else printf "$(gettext "Trying to move generated artifacts to '%s'...\n")" "$out_target_abs" mkdir -p -- "$out_target_abs/dm" "$out_target_abs/qr" mv staging_dm/*.png "$out_target_abs/dm/" mv staging_qr/*.png "$out_target_abs/qr/" cd -- "$orig_pwd" fi CLEAN_EXIT="true" if [ -z "$perm_dir" ]; then trap - EXIT INT TERM HUP cleanup_encode fi } # ------------------------------------------------------------------------- ## Sub-Function: Extractor Phase (Video -> .bin chunks) # ------------------------------------------------------------------------- extract_frames_to_bins() { input_video="$1" tmp_dir="$2" bin_staging="$3" grayscale_mode="$4" if [ -d "$input_video" ]; then printf "$(gettext "Input seems to be a directory. Reading frames from '%s'...\n")" "$input_video" frame_source="$input_video" else echo "$(gettext "Attempting frame extraction from video...")" pix_fmt_opt="" if [ "$grayscale_mode" = "1" ]; then pix_fmt_opt="-pix_fmt gray" fi ffmpeg -i "$input_video" $pix_fmt_opt "$tmp_dir/f_%08d.png" frame_source="$tmp_dir" fi echo "$(gettext "Attempting binary chunk extraction from frames...")" MAX_JOBS=$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1) FAILED_MARKER="$tmp_dir/.extract_failed" rm -f "$FAILED_MARKER" job_count=0 for img in "$frame_source"/*.png; do [ -f "$img" ] || continue ( ## Note: exit calls here only terminate the subshell, not the main script. ZXING_DM=$(ZXingReader -format DataMatrix -single "$img" 2>/dev/null) || exit 0 HEX_BYTES=$(printf '%s\n' "$ZXING_DM" | sed -n 's/^Bytes: *//p' | tr -d '\r\n') || exit 0 if [ -n "$HEX_BYTES" ]; then img_base=$(basename "$img") tmp_chunk="$bin_staging/tmp_${img_base}.bin" PRINTF_FMT=$(hex_to_octal_fmt "$HEX_BYTES") printf '%b' "$PRINTF_FMT" > "$tmp_chunk" || { touch "$FAILED_MARKER"; exit 1; } ## Prevent failure to find a QR code from aborting the subshell before cleanup ZXING_QR=$(ZXingReader -format QRCode -single "$img" 2>/dev/null) || : QR_CONTENT=$(printf '%s\n' "$ZXING_QR" | sed -n 's/^Text: *"\([^"]*\)".*/\1/p' | tr -d '\r\n') if [ -n "$QR_CONTENT" ]; then mv -- "$tmp_chunk" "$bin_staging/${QR_CONTENT}.bin" || { touch "$FAILED_MARKER"; exit 1; } else rm -f -- "$tmp_chunk" fi fi ) & job_count=$((job_count + 1)) if [ "$job_count" -ge "$MAX_JOBS" ]; then wait job_count=0 fi done wait if [ -f "$FAILED_MARKER" ]; then echo "$(gettext "Error: One or more frame extractions or file moves may have failed.")" >&2 rm -f "$FAILED_MARKER" exit 1 fi } # ------------------------------------------------------------------------- ## Sub-Function: Reassembler Phase (.bin chunks -> Target File) # ------------------------------------------------------------------------- reassemble_bins_to_file() { bin_staging="$1" output_dir="$2" raw_mode="$3" tmp_dir="$4" keep_intermediate="$5" processed_targets="$tmp_dir/processed_targets" : > "$processed_targets" for filepath in "$bin_staging"/*.bin; do [ -f "$filepath" ] || continue filename=${filepath##*/} tmp_name=${filename#*.} target_name=${tmp_name%.bin} [ "$target_name" != "$filename" ] && printf '%s\n' "$target_name" >> "$processed_targets" done if [ -s "$processed_targets" ]; then unique_targets=$(LC_ALL=C sort -u "$processed_targets") else echo "$(gettext "No valid data streams found from video.")" return 0 fi while read -r target; do [ -z "$target" ] && continue metadata_chunk="$bin_staging/00000000.${target}.bin" if [ ! -f "$metadata_chunk" ]; then printf "$(gettext "Must skip %s: Metadata chunk (sequence number 0) seems to be missing, cannot reassemble safely.\n")" "$target" continue fi filesize_line=$(sed -n '3p' "$metadata_chunk") filesize=$(echo "$filesize_line" | awk '{print $1}') chunk_size_line=$(sed -n '4p' "$metadata_chunk") chunk_size=$(echo "$chunk_size_line" | awk '{print $3}') if [ -z "$filesize" ] || [ -z "$chunk_size" ]; then printf "$(gettext "Must skip %s: Metadata seems corrupted (could not find filesize or chunk_size).\n")" "$target" continue fi expected_chunks=$(( (filesize + chunk_size - 1) / chunk_size )) printf "$(gettext "Attempting reassembly of %s (%s bytes, %s chunks)...\n")" "$target" "$filesize" "$expected_chunks" output_file="$output_dir/$target" : > "$output_file" corrupted_chunks="false" i=1 while [ "$i" -le "$expected_chunks" ]; do seq_num=$(printf "%08d" "$i") chunk_path="$bin_staging/${seq_num}.${target}.bin" if [ -f "$chunk_path" ]; then cat "$chunk_path" >> "$output_file" else printf "$(gettext " Warning: Chunk %s missing. Will try to pad with zeros.\n")" "$seq_num" corrupted_chunks="true" if [ "$i" -eq "$expected_chunks" ]; then pad_size=$(( filesize - (expected_chunks - 1) * chunk_size )) else pad_size="$chunk_size" fi dd if=/dev/zero bs=1 count="$pad_size" >> "$output_file" 2>/dev/null fi i=$((i + 1)) done printf "$(gettext "Reassembly attempt done, check output: %s\n")" "$target" if [ "$raw_mode" != "true" ]; then printf "$(gettext "Attempting to extract %s...\n")" "$target" tar -xf "$output_file" -C "$output_dir" target_basename="${target%.tar}" extracted_dir="$output_dir/$target_basename" repair_success=0 if [ "$corrupted_chunks" = "true" ]; then if [ -d "$extracted_dir" ] && [ -f "$extracted_dir/videostore.par2" ]; then echo "$(gettext "Missing chunks detected during reassembly! Attempting par2 repair...")" if (cd "$extracted_dir" && par2 repair videostore.par2); then repair_success=1 else echo "$(gettext "par2 repair seemingly failed. Will try to keep intermediate files for future recovery.")" fi else echo "$(gettext "Warning: no videostore.par2 file was found to repair the damage.")" fi else echo "$(gettext "All chunks arrived intact. Could skip par2 repair.")" repair_success=1 fi if [ "$keep_intermediate" != "true" ] && [ "$repair_success" -eq 1 ]; then rm -f "$output_file" [ -d "$extracted_dir" ] && rm -f "$extracted_dir"/videostore*.par2 fi fi done <<VAR_EOF $unique_targets VAR_EOF } # ------------------------------------------------------------------------- ## Core Decoder Router # ------------------------------------------------------------------------- decode_and_reassemble() { input_video="$1" output_dir="$2" perm_dir="$3" raw_mode="$4" bin_only="$5" grayscale_mode="$6" if [ ! -e "$input_video" ]; then printf "$(gettext "Error: Input path '%s' not found.\n")" "$input_video" >&2 exit 1 fi is_dir="false"; [ -d "$input_video" ] && is_dir="true" check_decoder_dependencies "$is_dir" "$raw_mode" mkdir -p -- "$output_dir" CLEAN_EXIT="false" cleanup_decoder() { trap - EXIT INT TERM HUP if [ "$CLEAN_EXIT" != "true" ]; then echo "" >&2 echo "$(gettext "EXIT: Attempting to clean up temporary files…")" >&2 fi _pids=$(jobs -p) [ -n "$_pids" ] && kill $_pids 2>/dev/null || true [ -d "$tmp_dir" ] && rm -rf "$tmp_dir" } keep_intermediate="false" if [ -n "$perm_dir" ]; then tmp_dir="$perm_dir" mkdir -p -- "$tmp_dir" keep_intermediate="true" else tmp_dir=$(mktemp -d) trap cleanup_decoder EXIT INT TERM HUP fi if [ -z "$grayscale_mode" ] && [ ! -d "$input_video" ]; then echo "=========================================================================" echo "$(gettext " Does the data matrix sequence use color?")" echo "$(gettext " (Color backgrounds are a huge waste of I/O overhead")" echo "$(gettext " unless the payload is color too)")" echo "" echo "$(gettext " Enter 1 for Grayscale")" echo "$(gettext " Enter 0 for Color")" echo "=========================================================================" printf "$(gettext "Selection [0/1]: ")" read -r grayscale_mode fi if [ "$bin_only" = "true" ] && [ -d "$input_video" ]; then printf "$(gettext "Bin-only mode: Using existing bin directory '%s' for reassembly.\n")" "$input_video" bin_staging="$input_video" else bin_staging="$tmp_dir/extracted_chunks" mkdir -p -- "$bin_staging" extract_frames_to_bins "$input_video" "$tmp_dir" "$bin_staging" "$grayscale_mode" if [ "$bin_only" = "true" ]; then printf "$(gettext "Bin-only mode: Copying extracted chunks to '%s'...\n")" "$output_dir" cp -r "$bin_staging/." "$output_dir/" printf "$(gettext "Bin extraction attempted, check output: '%s'.\n")" "$output_dir" CLEAN_EXIT="true" if [ -z "$perm_dir" ]; then trap - EXIT INT TERM HUP cleanup_decoder fi return 0 fi fi reassemble_bins_to_file "$bin_staging" "$output_dir" "$raw_mode" "$tmp_dir" "$keep_intermediate" CLEAN_EXIT="true" printf "$(gettext "Output ready to check: '%s'.\n")" "$output_dir" if [ -z "$perm_dir" ]; then trap - EXIT INT TERM HUP cleanup_decoder fi } ## --- Main CLI Parser & Router --- INPUT_FILE="" INPUT_VIDEO="" OUTPUT_TARGET="" FG_COLOR="000000" BG_COLOR="6B6B6B" BARCODE_LOOPS=0 THROUGHPUT_LEVEL=8 EXEC_MODE="" PERM_DIR="" L_FLAG_SET="false" IMAGES_ONLY="false" RAW_MODE="false" BIN_ONLY="false" GRAYSCALE_MODE="" GENERATE_SCRIPTS="1" PAR2_BLOCK_SIZE=1500 while [ $# -gt 0 ]; do case "$1" in -h|--help|-v|--version) usage 0 ;; -e) if [ -n "$EXEC_MODE" ]; then echo "$(gettext "Error: Cannot mix -e and -d")" >&2; usage; fi if [ -z "$2" ] || [ "${2#-}" != "$2" ]; then echo "$(gettext "Error: -e requires an argument.")" >&2; usage; fi INPUT_FILE="$2" EXEC_MODE="ENCODE" shift 2 ;; -d) if [ -n "$EXEC_MODE" ]; then echo "$(gettext "Error: Cannot mix -e and -d")" >&2; usage; fi if [ -z "$2" ] || [ "${2#-}" != "$2" ]; then echo "$(gettext "Error: -d requires an argument.")" >&2; usage; fi INPUT_VIDEO="$2" EXEC_MODE="DECODE" shift 2 ;; -b) BIN_ONLY="true" shift ;; -g) if [ -z "$2" ] || ! echo "$2" | grep -E -q '^[01]$'; then echo "$(gettext "Error: -g requires 0 or 1.")" >&2 usage fi GRAYSCALE_MODE="$2" shift 2 ;; -r) RAW_MODE="true" shift ;; -i) IMAGES_ONLY="true" shift ;; -k) if [ -z "$2" ] || ! echo "$2" | grep -E -q '^[01]$'; then echo "$(gettext "Error: -k requires 0 or 1.")" >&2 usage fi GENERATE_SCRIPTS="$2" shift 2 ;; -o) if [ -z "$2" ] || [ "${2#-}" != "$2" ]; then echo "$(gettext "Error: -o requires a directory.")" >&2; usage; fi PERM_DIR="$2" shift 2 ;; -c) if [ -z "$2" ] || [ "${2#-}" != "$2" ] || [ -z "$3" ] || [ "${3#-}" != "$3" ]; then echo "$(gettext "Error: -c requires two hex colors.")" >&2 usage fi if ! echo "$2" | grep -Eq '^[0-9a-fA-F]{6}$' || ! echo "$3" | grep -Eq '^[0-9a-fA-F]{6}$'; then echo "$(gettext "Error: Colors must be valid 6-digit hex values.")" >&2 usage fi FG_COLOR="$2" BG_COLOR="$3" shift 3 ;; -l) if [ -z "$2" ] || [ "${2#-}" != "$2" ] || ! echo "$2" | grep -E -q '^[0-9]+$'; then echo "$(gettext "Error: -l requires a valid non-negative integer.")" >&2 usage fi BARCODE_LOOPS="$2" L_FLAG_SET="true" shift 2 ;; -s) if [ -z "$2" ] || [ "${2#-}" != "$2" ] || ! echo "$2" | grep -E -q '^[0-9]+$'; then echo "$(gettext "Error: -s requires a valid non-negative integer block size.")" >&2 usage fi PAR2_BLOCK_SIZE="$2" shift 2 ;; -t) if [ -z "$2" ] || ! echo "$2" | grep -E -q '^[0-9]+$'; then echo "$(gettext "Error: -t requires an integer level (0-11).")" >&2 usage fi THROUGHPUT_LEVEL="$2" shift 2 ;; -*) printf "$(gettext "Unknown option: %s\n")" "$1" >&2 usage ;; *) if [ -n "$OUTPUT_TARGET" ]; then echo "$(gettext "Error: Too many arguments.")" >&2; usage; fi OUTPUT_TARGET="$1" shift ;; esac done if [ -z "$EXEC_MODE" ] || [ -z "$OUTPUT_TARGET" ]; then echo "$(gettext "Error: Missing operational mode (-e/-d) or final output target.")" >&2 usage fi ZINT_DM_SCALE=3 case "$THROUGHPUT_LEVEL" in 0) CHUNK_SIZE=40 ;; 1) CHUNK_SIZE=58 ;; 2) CHUNK_SIZE=82 ;; 3) CHUNK_SIZE=110 ;; 4) CHUNK_SIZE=140 ;; 5) CHUNK_SIZE=170 ;; 6) CHUNK_SIZE=200 ;; 7) CHUNK_SIZE=276 ;; 8) CHUNK_SIZE=364 ;; 9) CHUNK_SIZE=452; ZINT_DM_SCALE=2.5 ;; 10) CHUNK_SIZE=572; ZINT_DM_SCALE=2.5 ;; 11) CHUNK_SIZE=692; ZINT_DM_SCALE=2.5 ;; 12) CHUNK_SIZE=812; ZINT_DM_SCALE=2 ;; 13) CHUNK_SIZE=1046; ZINT_DM_SCALE=2 ;; 14) CHUNK_SIZE=1234; ZINT_DM_SCALE=1.5 ;; 15) CHUNK_SIZE=1554; ZINT_DM_SCALE=1.5 ;; *) echo "$(gettext "Error: Throughput level must be between 0 and 15.")"; usage ;; esac ## Execute routing if [ "$EXEC_MODE" = "ENCODE" ]; then encode_dm "$INPUT_FILE" "$OUTPUT_TARGET" "$FG_COLOR" "$BG_COLOR" "$BARCODE_LOOPS" "$CHUNK_SIZE" "$ZINT_DM_SCALE" "$PERM_DIR" "$L_FLAG_SET" "$IMAGES_ONLY" "$RAW_MODE" "$PAR2_BLOCK_SIZE" "$GENERATE_SCRIPTS" printf "$(gettext "Output ready to check: '%s'.\n")" "$OUTPUT_TARGET" elif [ "$EXEC_MODE" = "DECODE" ]; then decode_and_reassemble "$INPUT_VIDEO" "$OUTPUT_TARGET" "$PERM_DIR" "$RAW_MODE" "$BIN_ONLY" "$GRAYSCALE_MODE" fi if [ -n "$START_TIME" ]; then END_TIME=$(date +%s 2>/dev/null) || END_TIME="" if [ -n "$END_TIME" ]; then TOTAL_TIME=$((END_TIME - START_TIME)) printf "$(gettext "Total runtime: %s seconds.\n")" "$TOTAL_TIME" else echo "$(gettext "Notice: Failed to capture total script runtime.")" fi fi if [ "$EXEC_MODE" = "ENCODE" ]; then case "$OUTPUT_TARGET" in *.mkv) echo "=========================================================================" echo "$(gettext " USAGE TIPS FOR YOUR OUTPUT VIDEO:")" echo "$(gettext " 1. At throughput levels 0-8 (6x6 pixels per square),")" echo "$(gettext " scaling should be incremented in multiples of one sixth,")" echo "$(gettext " with NO interpolation, to retain sharp pixel edges.")" echo "$(gettext " (Level 9: 5x5 / Levels 12+ unreliably small, max 4x4)")" echo "$(gettext " 2. Use lossless encoding when uploading to apps which add")" echo "$(gettext " their own compression, such as YouTube.")" echo "$(gettext " 3. Test-decode your own file before publishing!")" echo "=========================================================================" ;; esac fi