Skip to content

Instantly share code, notes, and snippets.

@ethan605
Last active May 12, 2025 12:45
Show Gist options
  • Save ethan605/b6888f3c0e12e4f8168baf97f2164750 to your computer and use it in GitHub Desktop.
Save ethan605/b6888f3c0e12e4f8168baf97f2164750 to your computer and use it in GitHub Desktop.
qrgpg - encode/decode ASCII armoured file to/from QRCode images
#!/usr/bin/env bash
set -Eeuo pipefail
readonly RED=$(tput setaf 1)
readonly GREEN=$(tput setaf 2)
readonly YELLOW=$(tput setaf 3)
readonly NORMAL=$(tput sgr0)
readonly COMMAND="qrgpg"
SUB_COMMAND=""
INPUT_FILES=()
TOTAL_BLOCKS=7
OUTPUT_FILE="qrgpg.asc"
OUTPUT_PREFIX="qrgpg"
FONT_FILE=""
function print_help_message() {
local error_message
error_message=${1-}
if [[ -n $error_message ]]; then
printf "${RED}Error:${NORMAL} $error_message\n\n" >&2
fi
printf "$COMMAND - encode/decode ASCII armoured file to/from QRCode images
Usage:
$COMMAND encode [options] [flags] file
or $COMMAND decode [options] [flags] file...
Example:
$COMMAND encode -b 8 -p gpg private_key.asc
$COMMAND encode -f /path/to/custom/font private_key.asc
$COMMAND decode -o gpg.asc *.png
Available options for 'encode' mode:
-b, --blocks <string> Number of blocks to be encoded to, default to '7'.
-f, --font-file <file> Path to font file used for output image embedded caption.
-p, --output-prefix <string> String to be prepended to output image files, default to 'qrgpg'.
Available options for 'decode' mode:
-o, --output-file <string> Output file name, default to 'qrgpg.asc'.
Available flags
-h, --help Print this message
"
}
function parse_option_arg() {
if [[ -n "${2-}" ]] && [[ ${2:0:1} != "-" ]]; then
echo "$2"
else
print_help_message "Argument for $1 is missing"
exit 1
fi
}
function parse_args() {
while (("$#")); do
case "$1" in
encode | decode)
SUB_COMMAND=$1
shift
;;
-b | --blocks)
TOTAL_BLOCKS=$(parse_option_arg "$@")
shift 2
;;
-f | --font-file)
FONT_FILE=$(parse_option_arg "$@")
shift 2
;;
-p | --output-prefix)
OUTPUT_PREFIX=$(parse_option_arg "$@")
shift 2
;;
-o | --output-file)
OUTPUT_FILE=$(parse_option_arg "$@")
shift 2
;;
-h | --help)
print_help_message
exit 1
;;
-* | --*=) # unsupported args
print_help_message "Unsupported argument $1"
exit 1
;;
*) # preserve positional args
INPUT_FILES+=("$1")
shift
;;
esac
done
if [[ $SUB_COMMAND != "encode" ]] && [[ $SUB_COMMAND != "decode" ]]; then
print_help_message "Unsupported sub-command"
exit 1
fi
if [[ ${#INPUT_FILES[@]} == 0 ]]; then
if [[ $SUB_COMMAND == "encode" ]]; then
print_help_message "Missing input file"
fi
if [[ $SUB_COMMAND == "decode" ]]; then
print_help_message "Missing files list"
fi
exit 1
fi
}
function zero_padding() {
printf "%02d" "$1"
}
function print_check_result() {
local result=${1:-""}
[[ -n $result ]] && echo "${GREEN}✔︎${NORMAL}" || echo "${RED}✗${NORMAL}"
}
function check_dependencies() {
local qrencode_check zbarimg_check imagemagick_check
qrencode_check=$(command -v qrencode)
zbarimg_check=$(command -v zbarimg)
imagemagick_check=$(command -v magick)
if [[ -z $qrencode_check || -z $zbarimg_check || -z $imagemagick_check ]]; then
printf "${RED}Error:${NORMAL} following dependencies are required:
$(print_check_result $qrencode_check) qrencode (https://fukuchi.org/works/qrencode)
$(print_check_result $zbarimg_check) zbarimg (https://github.com/mchehab/zbar)
$(print_check_result $imagemagick_check) imagemagick (https://imagemagick.org)
"
exit 1
fi
}
function generate_qr() {
local block_order blocks_count block_data prefix out_file
block_order=$(zero_padding "$1")
blocks_count=$(zero_padding "$2")
block_data=$3
prefix=$4
out_file="${prefix}_${block_order}.png"
echo -e "$block_data" |
qrencode \
--dpi=300 \
--level=H \
--size=4 \
--output="$out_file"
if [[ -n $FONT_FILE ]]; then
magick "$out_file" \
-gravity South \
-splice 0x15 \
-font "$FONT_FILE" \
-annotate +0+10 "$prefix ${block_order}/$blocks_count" "$out_file"
else
magick "$out_file" \
-gravity South \
-splice 0x15 \
-annotate +0+10 "$prefix ${block_order}/$blocks_count" "$out_file"
fi
printf "${GREEN}Block $block_order${NORMAL} successfully encoded into image $out_file\n"
}
function calc_lines_per_block() {
local input_file blocks_count total_lines lines_per_block
input_file=$1
blocks_count=$2
total_lines=$(wc -l <"$input_file" | grep --only-matching --extended-regexp "[0-9]+")
lines_per_block=$(("$total_lines" / "$blocks_count"))
echo $lines_per_block
}
function encode() {
local input_file lines_per_block block_data block_order line_number
input_file=${INPUT_FILES[0]}
lines_per_block=$(calc_lines_per_block "$input_file" "$TOTAL_BLOCKS")
block_data=""
block_order=1
line_number=1
while IFS= read -r line_data; do
if [[ $line_number -eq 1 ]]; then
block_data=$line_data
else
block_data+="\n$line_data"
fi
((line_number++))
if [[ $line_number -gt $lines_per_block && $block_order -lt $TOTAL_BLOCKS ]]; then
generate_qr "$block_order" "$TOTAL_BLOCKS" "$block_data" "$OUTPUT_PREFIX"
((block_order++))
((line_number = 1))
block_data=""
fi
done <"$input_file"
generate_qr "$block_order" "$TOTAL_BLOCKS" "$block_data" "$OUTPUT_PREFIX"
printf "\n${GREEN}Encoding done!${NORMAL}\n"
}
function decode() {
local data decoded_data
data=""
for input_file in "${INPUT_FILES[@]}"; do
printf "${GREEN}Decoding${NORMAL} $input_file\n"
decoded_data=$(zbarimg --quiet --raw "$input_file")
data+="$decoded_data\n"
done
echo -e "$data" >"$OUTPUT_FILE"
printf "\n${GREEN}Decoding done!${NORMAL} Data written to $OUTPUT_FILE\n"
}
function main() {
check_dependencies
parse_args "$@"
case "$SUB_COMMAND" in
encode) encode ;;
decode) decode ;;
esac
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment