Last active
May 12, 2025 12:45
-
-
Save ethan605/b6888f3c0e12e4f8168baf97f2164750 to your computer and use it in GitHub Desktop.
qrgpg - encode/decode ASCII armoured file to/from QRCode images
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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