pkpass2png.sh
· 8.2 KiB · Bash
Raw
#!/usr/bin/env bash
set -euo pipefail
# Usage:
# ./pkpass2png <pkpass_path_or_url> [output.png]
#
# Examples:
# ./pkpass2png ticket.pkpass out.png
# ./pkpass2png "https://example.com/ticket.pkpass" out.png
INPUT="${1:-}"
OUT="${2:-ticket.png}"
if [[ -z "$INPUT" ]]; then
echo "Usage: $0 <pkpass_path_or_url> [output.png]" >&2
exit 2
fi
need_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "Missing dependency: $1" >&2
exit 2
}
}
need_cmd jq
need_cmd zint
need_cmd magick
need_cmd unzip
# Pick a downloader if needed
is_url=0
if [[ "$INPUT" =~ ^https?:// ]]; then
is_url=1
if command -v curl >/dev/null 2>&1; then
DL='curl -L --fail --silent --show-error'
elif command -v wget >/dev/null 2>&1; then
DL='wget -qO-'
else
echo "Need curl or wget to download URLs" >&2
exit 2
fi
fi
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
PKPASS_FILE="$tmp/pass.pkpass"
EXTRACT_DIR="$tmp/extracted"
mkdir -p "$EXTRACT_DIR"
# ---- Acquire pkpass (download or copy) ----
if [[ "$is_url" -eq 1 ]]; then
# download to file
if [[ "$DL" == curl* ]]; then
$DL "$INPUT" -o "$PKPASS_FILE"
else
$DL "$INPUT" > "$PKPASS_FILE"
fi
else
# local file
if [[ ! -f "$INPUT" ]]; then
echo "Input file not found: $INPUT" >&2
exit 2
fi
cp -f "$INPUT" "$PKPASS_FILE"
fi
# ---- Extract pkpass (zip) ----
unzip -q "$PKPASS_FILE" -d "$EXTRACT_DIR"
PASS_JSON="$EXTRACT_DIR/pass.json"
if [[ ! -f "$PASS_JSON" ]]; then
echo "pass.json not found inside pkpass" >&2
exit 2
fi
# Choose base image: prefer strip.png if present, else background.png
if [[ -f "$EXTRACT_DIR/strip.png" ]]; then
BASE_IMG="$EXTRACT_DIR/strip.png"
elif [[ -f "$EXTRACT_DIR/background.png" ]]; then
BASE_IMG="$EXTRACT_DIR/background.png"
else
echo "Neither strip.png nor background.png found in pkpass" >&2
exit 2
fi
# Work files
MSG_FILE="$tmp/msg.txt"
BARCODE_RAW="$tmp/barcode_raw.png"
BARCODE_CARD="$tmp/barcode_card.png"
font="DejaVu-Sans"
# Find largest pointsize <= max_pt that fits within max_width pixels
fit_pt() {
local text="$1" max_width="$2" max_pt="$3" font="$4"
local pt w
for ((pt=max_pt; pt>=9; pt--)); do
w=$(magick -font "$font" -pointsize "$pt" \
-background none label:"$text" -format '%w' info:)
if [ "$w" -le "$max_width" ]; then
echo "$pt"
return
fi
done
echo 9
}
# ---- Extract fields ----
org=$(jq -r '.organizationName // ""' "$PASS_JSON")
logoText=$(jq -r '.logoText // empty' "$PASS_JSON")
date=$(jq -r '.eventTicket.headerFields[]? | select(.key=="date") | .value' "$PASS_JSON" | head -n1)
event=$(jq -r '.eventTicket.primaryFields[]? | select(.key=="event") | .value' "$PASS_JSON" | head -n1)
loc_label=$(jq -r '.eventTicket.secondaryFields[]? | select(.key=="location") | .label' "$PASS_JSON" | head -n1)
loc_value=$(jq -r '.eventTicket.secondaryFields[]? | select(.key=="location") | .value' "$PASS_JSON" | head -n1)
seat_label=$(jq -r '.eventTicket.secondaryFields[]? | select(.key=="seat") | .label' "$PASS_JSON" | head -n1)
seat_value=$(jq -r '.eventTicket.secondaryFields[]? | select(.key=="seat") | .value' "$PASS_JSON" | head -n1)
price_label=$(jq -r '.eventTicket.auxiliaryFields[]? | select(.key=="price") | .label' "$PASS_JSON" | head -n1)
price_value=$(jq -r '.eventTicket.auxiliaryFields[]? | select(.key=="price") | .value' "$PASS_JSON" | head -n1)
alt_text=$(jq -r '.barcode.altText // ""' "$PASS_JSON")
fg=$(jq -r '.foregroundColor // "rgb(255,255,255)"' "$PASS_JSON")
label=$(jq -r '.labelColor // "rgb(200,200,200)"' "$PASS_JSON")
# ---- Canvas size ----
W=$(magick identify -format '%w' "$BASE_IMG") # [1]
H=$(magick identify -format '%h' "$BASE_IMG") # [1]
# ---- Proportional layout constants ----
# Margins/padding (percentages tuned to your 360x440 case)
M=$(( W * 4 / 100 )) # ~4% of width (360 -> 14)
[ "$M" -lt 10 ] && M=10
TOP=$(( H * 3 / 100 )) # ~3% of height (440 -> 13)
COL_GAP=$(( W * 4 / 100 )) # ~4% of width
[ "$COL_GAP" -lt 10 ] && COL_GAP=10
COL_W=$(((W - 2*M - COL_GAP)/2))
# Barcode sizing derived from width
BC_SIZE=$(( W * 52 / 100 )) # 360 -> 187
[ "$BC_SIZE" -lt 150 ] && BC_SIZE=150 # keep scannable
BOTTOM_MARGIN=$(( H * 4 / 100 )) # 440 -> 17
[ "$BOTTOM_MARGIN" -lt 10 ] && BOTTOM_MARGIN=10
BAR_GAP=$(( H * 3 / 100 )) # 440 -> 13
[ "$BAR_GAP" -lt 10 ] && BAR_GAP=10
# Font maxima (scale with height)
org_max=$(( H * 4 / 100 )) # 440 -> 17
date_max=$(( H * 3 / 100 )) # 440 -> 13
event_max=$(( H * 5 / 100 )) # 440 -> 22
val_max=$(( H * 4 / 100 )) # 440 -> 17
[ "$org_max" -lt 12 ] && org_max=12
[ "$date_max" -lt 11 ] && date_max=11
[ "$event_max" -lt 14 ] && event_max=14
[ "$val_max" -lt 12 ] && val_max=12
label_pt=$(( H * 3 / 100 )) # 440 -> 13
[ "$label_pt" -lt 11 ] && label_pt=11
# Vertical gaps (scale with height)
gap1=$(( H * 7 / 100 )) # 440 -> 30
gap2=$(( H * 7 / 100 )) # 440 -> 30
gap3=$(( H * 4 / 100 )) # 440 -> 17
gap4=$(( H * 6 / 100 )) # 440 -> 22
gap5=$(( H * 4 / 100 )) # 440 -> 17
# Baseline clamp (prevents top clipping)
MIN_Y1=$(( H * 5 / 100 )) # 440 -> 30
[ "$MIN_Y1" -lt 18 ] && MIN_Y1=18
# ---- Barcode (Aztec) ----
jq -r '.barcode.message' "$PASS_JSON" | iconv -f UTF-8 -t ISO-8859-1 > "$MSG_FILE"
zint --barcode=92 --scale=5 --border=1 -o "$BARCODE_RAW" -i "$MSG_FILE" # [1]
magick "$BARCODE_RAW" \
-resize "${BC_SIZE}x${BC_SIZE}" \
-background white -gravity south -splice 0x$((H*5/100)) \
-fill black -font "$font" -pointsize $((H*3/100)) \
-annotate +0+0 "$alt_text" \
-bordercolor white -border $((W*3/100)) \
"$BARCODE_CARD" # [1]
BAR_H=$(magick identify -format '%h' "$BARCODE_CARD") # [1]
TEXT_H=$(( H - BAR_H - BOTTOM_MARGIN - BAR_GAP )) # [1]
# ---- Auto-fit point sizes ----
# Leave room for date on the right: reserve ~40% width for it
org_pt=$(fit_pt "${logoText:-$org}" $((W - 2*M - (W*40/100))) "$org_max" "$font") # [1]
date_pt=$(fit_pt "$date" $((W - 2*M)) "$date_max" "$font") # [1]
event_pt=$(fit_pt "$event" $((W - 2*M)) "$event_max" "$font") # [1]
loc_val_pt=$(fit_pt "$loc_value" "$COL_W" "$val_max" "$font") # [1]
seat_val_pt=$(fit_pt "$seat_value" "$COL_W" "$val_max" "$font") # [1]
price_val_pt=$(fit_pt "$price_value" $((W - 2*M)) "$val_max" "$font") # [1]
# ---- Y positions ----
y1=$((TOP + (H*6/100))) # org/date baseline
y2=$((y1 + gap1)) # event
y3=$((y2 + gap2)) # secondary labels
y4=$((y3 + gap3)) # secondary values
y5=$((y4 + gap4)) # price label
y6=$((y5 + gap5)) # price value
# Shift up if we collide with barcode reserved area (conservative)
max_y=$((y6 + (H*7/100)))
if [ "$max_y" -ge "$TEXT_H" ]; then
shift=$((max_y - TEXT_H + (H*1/100)))
y1=$((y1 - shift))
y2=$((y2 - shift))
y3=$((y3 - shift))
y4=$((y4 - shift))
y5=$((y5 - shift))
y6=$((y6 - shift))
fi
# Clamp top baseline so first line doesn't clip
if [ "$y1" -lt "$MIN_Y1" ]; then
d=$((MIN_Y1 - y1))
y1=$((y1 + d))
y2=$((y2 + d))
y3=$((y3 + d))
y4=$((y4 + d))
y5=$((y5 + d))
y6=$((y6 + d))
fi
# ---- Compose ----
magick "$BASE_IMG" \
-font "$font" \
\
-gravity northwest \
-fill "$fg" -pointsize "$org_pt" \
-annotate +$M+$y1 "${logoText:-$org}" \
\
-gravity northeast \
-fill "$fg" -pointsize "$date_pt" \
-annotate +$M+$y1 "$date" \
\
-gravity northwest \
-fill "$fg" -pointsize "$event_pt" \
-annotate +$M+$y2 "$event" \
\
-fill "$fg" -pointsize "$label_pt" \
-annotate +$M+$y3 "$loc_label" \
\
-gravity northeast \
-fill "$fg" -pointsize "$label_pt" \
-annotate +$M+$y3 "$seat_label" \
\
-gravity northwest \
-fill "$label" -pointsize "$loc_val_pt" \
-annotate +$M+$y4 "$loc_value" \
\
-gravity northeast \
-fill "$label" -pointsize "$seat_val_pt" \
-annotate +$M+$y4 "$seat_value" \
\
-gravity northwest \
-fill "$fg" -pointsize "$label_pt" \
-annotate +$M+$y5 "$price_label" \
\
-fill "$label" -pointsize "$price_val_pt" \
-annotate +$M+$y6 "$price_value" \
\
\( "$BARCODE_CARD" \) \
-gravity south -geometry +0+$BOTTOM_MARGIN -composite \
\
"$OUT"
| 1 | #!/usr/bin/env bash |
| 2 | set -euo pipefail |
| 3 | |
| 4 | # Usage: |
| 5 | # ./pkpass2png <pkpass_path_or_url> [output.png] |
| 6 | # |
| 7 | # Examples: |
| 8 | # ./pkpass2png ticket.pkpass out.png |
| 9 | # ./pkpass2png "https://example.com/ticket.pkpass" out.png |
| 10 | |
| 11 | INPUT="${1:-}" |
| 12 | OUT="${2:-ticket.png}" |
| 13 | |
| 14 | if [[ -z "$INPUT" ]]; then |
| 15 | echo "Usage: $0 <pkpass_path_or_url> [output.png]" >&2 |
| 16 | exit 2 |
| 17 | fi |
| 18 | |
| 19 | need_cmd() { |
| 20 | command -v "$1" >/dev/null 2>&1 || { |
| 21 | echo "Missing dependency: $1" >&2 |
| 22 | exit 2 |
| 23 | } |
| 24 | } |
| 25 | |
| 26 | need_cmd jq |
| 27 | need_cmd zint |
| 28 | need_cmd magick |
| 29 | need_cmd unzip |
| 30 | |
| 31 | # Pick a downloader if needed |
| 32 | is_url=0 |
| 33 | if [[ "$INPUT" =~ ^https?:// ]]; then |
| 34 | is_url=1 |
| 35 | if command -v curl >/dev/null 2>&1; then |
| 36 | DL='curl -L --fail --silent --show-error' |
| 37 | elif command -v wget >/dev/null 2>&1; then |
| 38 | DL='wget -qO-' |
| 39 | else |
| 40 | echo "Need curl or wget to download URLs" >&2 |
| 41 | exit 2 |
| 42 | fi |
| 43 | fi |
| 44 | |
| 45 | tmp=$(mktemp -d) |
| 46 | trap 'rm -rf "$tmp"' EXIT |
| 47 | |
| 48 | PKPASS_FILE="$tmp/pass.pkpass" |
| 49 | EXTRACT_DIR="$tmp/extracted" |
| 50 | mkdir -p "$EXTRACT_DIR" |
| 51 | |
| 52 | # ---- Acquire pkpass (download or copy) ---- |
| 53 | if [[ "$is_url" -eq 1 ]]; then |
| 54 | # download to file |
| 55 | if [[ "$DL" == curl* ]]; then |
| 56 | $DL "$INPUT" -o "$PKPASS_FILE" |
| 57 | else |
| 58 | $DL "$INPUT" > "$PKPASS_FILE" |
| 59 | fi |
| 60 | else |
| 61 | # local file |
| 62 | if [[ ! -f "$INPUT" ]]; then |
| 63 | echo "Input file not found: $INPUT" >&2 |
| 64 | exit 2 |
| 65 | fi |
| 66 | cp -f "$INPUT" "$PKPASS_FILE" |
| 67 | fi |
| 68 | |
| 69 | # ---- Extract pkpass (zip) ---- |
| 70 | unzip -q "$PKPASS_FILE" -d "$EXTRACT_DIR" |
| 71 | |
| 72 | PASS_JSON="$EXTRACT_DIR/pass.json" |
| 73 | if [[ ! -f "$PASS_JSON" ]]; then |
| 74 | echo "pass.json not found inside pkpass" >&2 |
| 75 | exit 2 |
| 76 | fi |
| 77 | |
| 78 | # Choose base image: prefer strip.png if present, else background.png |
| 79 | if [[ -f "$EXTRACT_DIR/strip.png" ]]; then |
| 80 | BASE_IMG="$EXTRACT_DIR/strip.png" |
| 81 | elif [[ -f "$EXTRACT_DIR/background.png" ]]; then |
| 82 | BASE_IMG="$EXTRACT_DIR/background.png" |
| 83 | else |
| 84 | echo "Neither strip.png nor background.png found in pkpass" >&2 |
| 85 | exit 2 |
| 86 | fi |
| 87 | |
| 88 | # Work files |
| 89 | MSG_FILE="$tmp/msg.txt" |
| 90 | BARCODE_RAW="$tmp/barcode_raw.png" |
| 91 | BARCODE_CARD="$tmp/barcode_card.png" |
| 92 | font="DejaVu-Sans" |
| 93 | |
| 94 | # Find largest pointsize <= max_pt that fits within max_width pixels |
| 95 | fit_pt() { |
| 96 | local text="$1" max_width="$2" max_pt="$3" font="$4" |
| 97 | local pt w |
| 98 | for ((pt=max_pt; pt>=9; pt--)); do |
| 99 | w=$(magick -font "$font" -pointsize "$pt" \ |
| 100 | -background none label:"$text" -format '%w' info:) |
| 101 | if [ "$w" -le "$max_width" ]; then |
| 102 | echo "$pt" |
| 103 | return |
| 104 | fi |
| 105 | done |
| 106 | echo 9 |
| 107 | } |
| 108 | |
| 109 | # ---- Extract fields ---- |
| 110 | org=$(jq -r '.organizationName // ""' "$PASS_JSON") |
| 111 | logoText=$(jq -r '.logoText // empty' "$PASS_JSON") |
| 112 | date=$(jq -r '.eventTicket.headerFields[]? | select(.key=="date") | .value' "$PASS_JSON" | head -n1) |
| 113 | event=$(jq -r '.eventTicket.primaryFields[]? | select(.key=="event") | .value' "$PASS_JSON" | head -n1) |
| 114 | |
| 115 | loc_label=$(jq -r '.eventTicket.secondaryFields[]? | select(.key=="location") | .label' "$PASS_JSON" | head -n1) |
| 116 | loc_value=$(jq -r '.eventTicket.secondaryFields[]? | select(.key=="location") | .value' "$PASS_JSON" | head -n1) |
| 117 | |
| 118 | seat_label=$(jq -r '.eventTicket.secondaryFields[]? | select(.key=="seat") | .label' "$PASS_JSON" | head -n1) |
| 119 | seat_value=$(jq -r '.eventTicket.secondaryFields[]? | select(.key=="seat") | .value' "$PASS_JSON" | head -n1) |
| 120 | |
| 121 | price_label=$(jq -r '.eventTicket.auxiliaryFields[]? | select(.key=="price") | .label' "$PASS_JSON" | head -n1) |
| 122 | price_value=$(jq -r '.eventTicket.auxiliaryFields[]? | select(.key=="price") | .value' "$PASS_JSON" | head -n1) |
| 123 | |
| 124 | alt_text=$(jq -r '.barcode.altText // ""' "$PASS_JSON") |
| 125 | |
| 126 | fg=$(jq -r '.foregroundColor // "rgb(255,255,255)"' "$PASS_JSON") |
| 127 | label=$(jq -r '.labelColor // "rgb(200,200,200)"' "$PASS_JSON") |
| 128 | |
| 129 | # ---- Canvas size ---- |
| 130 | W=$(magick identify -format '%w' "$BASE_IMG") # [1] |
| 131 | H=$(magick identify -format '%h' "$BASE_IMG") # [1] |
| 132 | |
| 133 | # ---- Proportional layout constants ---- |
| 134 | # Margins/padding (percentages tuned to your 360x440 case) |
| 135 | M=$(( W * 4 / 100 )) # ~4% of width (360 -> 14) |
| 136 | [ "$M" -lt 10 ] && M=10 |
| 137 | |
| 138 | TOP=$(( H * 3 / 100 )) # ~3% of height (440 -> 13) |
| 139 | COL_GAP=$(( W * 4 / 100 )) # ~4% of width |
| 140 | [ "$COL_GAP" -lt 10 ] && COL_GAP=10 |
| 141 | COL_W=$(((W - 2*M - COL_GAP)/2)) |
| 142 | |
| 143 | # Barcode sizing derived from width |
| 144 | BC_SIZE=$(( W * 52 / 100 )) # 360 -> 187 |
| 145 | [ "$BC_SIZE" -lt 150 ] && BC_SIZE=150 # keep scannable |
| 146 | BOTTOM_MARGIN=$(( H * 4 / 100 )) # 440 -> 17 |
| 147 | [ "$BOTTOM_MARGIN" -lt 10 ] && BOTTOM_MARGIN=10 |
| 148 | BAR_GAP=$(( H * 3 / 100 )) # 440 -> 13 |
| 149 | [ "$BAR_GAP" -lt 10 ] && BAR_GAP=10 |
| 150 | |
| 151 | # Font maxima (scale with height) |
| 152 | org_max=$(( H * 4 / 100 )) # 440 -> 17 |
| 153 | date_max=$(( H * 3 / 100 )) # 440 -> 13 |
| 154 | event_max=$(( H * 5 / 100 )) # 440 -> 22 |
| 155 | val_max=$(( H * 4 / 100 )) # 440 -> 17 |
| 156 | [ "$org_max" -lt 12 ] && org_max=12 |
| 157 | [ "$date_max" -lt 11 ] && date_max=11 |
| 158 | [ "$event_max" -lt 14 ] && event_max=14 |
| 159 | [ "$val_max" -lt 12 ] && val_max=12 |
| 160 | |
| 161 | label_pt=$(( H * 3 / 100 )) # 440 -> 13 |
| 162 | [ "$label_pt" -lt 11 ] && label_pt=11 |
| 163 | |
| 164 | # Vertical gaps (scale with height) |
| 165 | gap1=$(( H * 7 / 100 )) # 440 -> 30 |
| 166 | gap2=$(( H * 7 / 100 )) # 440 -> 30 |
| 167 | gap3=$(( H * 4 / 100 )) # 440 -> 17 |
| 168 | gap4=$(( H * 6 / 100 )) # 440 -> 22 |
| 169 | gap5=$(( H * 4 / 100 )) # 440 -> 17 |
| 170 | |
| 171 | # Baseline clamp (prevents top clipping) |
| 172 | MIN_Y1=$(( H * 5 / 100 )) # 440 -> 30 |
| 173 | [ "$MIN_Y1" -lt 18 ] && MIN_Y1=18 |
| 174 | |
| 175 | # ---- Barcode (Aztec) ---- |
| 176 | jq -r '.barcode.message' "$PASS_JSON" | iconv -f UTF-8 -t ISO-8859-1 > "$MSG_FILE" |
| 177 | zint --barcode=92 --scale=5 --border=1 -o "$BARCODE_RAW" -i "$MSG_FILE" # [1] |
| 178 | |
| 179 | magick "$BARCODE_RAW" \ |
| 180 | -resize "${BC_SIZE}x${BC_SIZE}" \ |
| 181 | -background white -gravity south -splice 0x$((H*5/100)) \ |
| 182 | -fill black -font "$font" -pointsize $((H*3/100)) \ |
| 183 | -annotate +0+0 "$alt_text" \ |
| 184 | -bordercolor white -border $((W*3/100)) \ |
| 185 | "$BARCODE_CARD" # [1] |
| 186 | |
| 187 | BAR_H=$(magick identify -format '%h' "$BARCODE_CARD") # [1] |
| 188 | TEXT_H=$(( H - BAR_H - BOTTOM_MARGIN - BAR_GAP )) # [1] |
| 189 | |
| 190 | # ---- Auto-fit point sizes ---- |
| 191 | # Leave room for date on the right: reserve ~40% width for it |
| 192 | org_pt=$(fit_pt "${logoText:-$org}" $((W - 2*M - (W*40/100))) "$org_max" "$font") # [1] |
| 193 | date_pt=$(fit_pt "$date" $((W - 2*M)) "$date_max" "$font") # [1] |
| 194 | event_pt=$(fit_pt "$event" $((W - 2*M)) "$event_max" "$font") # [1] |
| 195 | |
| 196 | loc_val_pt=$(fit_pt "$loc_value" "$COL_W" "$val_max" "$font") # [1] |
| 197 | seat_val_pt=$(fit_pt "$seat_value" "$COL_W" "$val_max" "$font") # [1] |
| 198 | price_val_pt=$(fit_pt "$price_value" $((W - 2*M)) "$val_max" "$font") # [1] |
| 199 | |
| 200 | # ---- Y positions ---- |
| 201 | y1=$((TOP + (H*6/100))) # org/date baseline |
| 202 | y2=$((y1 + gap1)) # event |
| 203 | y3=$((y2 + gap2)) # secondary labels |
| 204 | y4=$((y3 + gap3)) # secondary values |
| 205 | y5=$((y4 + gap4)) # price label |
| 206 | y6=$((y5 + gap5)) # price value |
| 207 | |
| 208 | # Shift up if we collide with barcode reserved area (conservative) |
| 209 | max_y=$((y6 + (H*7/100))) |
| 210 | if [ "$max_y" -ge "$TEXT_H" ]; then |
| 211 | shift=$((max_y - TEXT_H + (H*1/100))) |
| 212 | y1=$((y1 - shift)) |
| 213 | y2=$((y2 - shift)) |
| 214 | y3=$((y3 - shift)) |
| 215 | y4=$((y4 - shift)) |
| 216 | y5=$((y5 - shift)) |
| 217 | y6=$((y6 - shift)) |
| 218 | fi |
| 219 | |
| 220 | # Clamp top baseline so first line doesn't clip |
| 221 | if [ "$y1" -lt "$MIN_Y1" ]; then |
| 222 | d=$((MIN_Y1 - y1)) |
| 223 | y1=$((y1 + d)) |
| 224 | y2=$((y2 + d)) |
| 225 | y3=$((y3 + d)) |
| 226 | y4=$((y4 + d)) |
| 227 | y5=$((y5 + d)) |
| 228 | y6=$((y6 + d)) |
| 229 | fi |
| 230 | |
| 231 | # ---- Compose ---- |
| 232 | magick "$BASE_IMG" \ |
| 233 | -font "$font" \ |
| 234 | \ |
| 235 | -gravity northwest \ |
| 236 | -fill "$fg" -pointsize "$org_pt" \ |
| 237 | -annotate +$M+$y1 "${logoText:-$org}" \ |
| 238 | \ |
| 239 | -gravity northeast \ |
| 240 | -fill "$fg" -pointsize "$date_pt" \ |
| 241 | -annotate +$M+$y1 "$date" \ |
| 242 | \ |
| 243 | -gravity northwest \ |
| 244 | -fill "$fg" -pointsize "$event_pt" \ |
| 245 | -annotate +$M+$y2 "$event" \ |
| 246 | \ |
| 247 | -fill "$fg" -pointsize "$label_pt" \ |
| 248 | -annotate +$M+$y3 "$loc_label" \ |
| 249 | \ |
| 250 | -gravity northeast \ |
| 251 | -fill "$fg" -pointsize "$label_pt" \ |
| 252 | -annotate +$M+$y3 "$seat_label" \ |
| 253 | \ |
| 254 | -gravity northwest \ |
| 255 | -fill "$label" -pointsize "$loc_val_pt" \ |
| 256 | -annotate +$M+$y4 "$loc_value" \ |
| 257 | \ |
| 258 | -gravity northeast \ |
| 259 | -fill "$label" -pointsize "$seat_val_pt" \ |
| 260 | -annotate +$M+$y4 "$seat_value" \ |
| 261 | \ |
| 262 | -gravity northwest \ |
| 263 | -fill "$fg" -pointsize "$label_pt" \ |
| 264 | -annotate +$M+$y5 "$price_label" \ |
| 265 | \ |
| 266 | -fill "$label" -pointsize "$price_val_pt" \ |
| 267 | -annotate +$M+$y6 "$price_value" \ |
| 268 | \ |
| 269 | \( "$BARCODE_CARD" \) \ |
| 270 | -gravity south -geometry +0+$BOTTOM_MARGIN -composite \ |
| 271 | \ |
| 272 | "$OUT" |
| 273 |