| #!/bin/bash | |
| set -euo pipefail | |
| usage() { | |
| cat <<EOF | |
| Usage: $0 [-o TESTS_FILE] [-n TEST_NAME] [-e EXPECTED_FILE] [--expected-dir DIR] [--subset-dir DIR] HB_VECTOR FONT_FILE [VECTOR_OPTIONS] [TEXT] | |
| Subset font, verify visual equivalence with hb-view, and append vector test line: | |
| font-file;options;unicodes;expected-file | |
| Options: | |
| -o TESTS_FILE Append test line to this file (default: stdout). | |
| -n TEST_NAME Test name used for default expected filename. | |
| -e EXPECTED_FILE Expected output file path. | |
| --expected-dir DIR Directory for auto-generated expected files, relative | |
| to TESTS_FILE directory (default: ../expected). | |
| --subset-dir DIR Directory for subset fonts, relative to TESTS_FILE | |
| directory (default: ../fonts). | |
| EOF | |
| } | |
| sha1_file() { | |
| if [ "$SHA1SUM_KIND" = "sha1sum" ]; then | |
| sha1sum "$1" | cut -d' ' -f1 | |
| elif [ "$SHA1SUM_KIND" = "shasum" ]; then | |
| shasum -a 1 "$1" | cut -d' ' -f1 | |
| else | |
| digest -a sha1 "$1" | cut -d' ' -f1 | |
| fi | |
| } | |
| is_vector_only_option() { | |
| case "$1" in | |
| -q|--quiet|--ned|--precision|--precision=*|-o|--output-file|--output-file=*|-O|--output-format|--output-format=*) | |
| return 0 | |
| ;; | |
| *) | |
| return 1 | |
| ;; | |
| esac | |
| } | |
| if which sha1sum >/dev/null 2>/dev/null; then | |
| SHA1SUM_KIND=sha1sum | |
| elif which shasum >/dev/null 2>/dev/null; then | |
| SHA1SUM_KIND=shasum | |
| elif which digest >/dev/null 2>/dev/null; then | |
| SHA1SUM_KIND=digest | |
| else | |
| echo "'sha1sum' not found" >&2 | |
| exit 2 | |
| fi | |
| out=/dev/stdout | |
| test_name= | |
| expected_file= | |
| expected_dir=../expected | |
| expected_dir_explicit=false | |
| subset_dir=../fonts | |
| while [ $# -gt 0 ]; do | |
| case "$1" in | |
| -o) | |
| out=$2 | |
| shift 2 | |
| ;; | |
| -n|--test-name) | |
| test_name=$2 | |
| shift 2 | |
| ;; | |
| -e|--expected-file) | |
| expected_file=$2 | |
| shift 2 | |
| ;; | |
| --expected-dir) | |
| expected_dir=$2 | |
| expected_dir_explicit=true | |
| shift 2 | |
| ;; | |
| --subset-dir) | |
| subset_dir=$2 | |
| shift 2 | |
| ;; | |
| -h|--help) | |
| usage | |
| exit 0 | |
| ;; | |
| --) | |
| shift | |
| break | |
| ;; | |
| *) | |
| break | |
| ;; | |
| esac | |
| done | |
| if [ $# -lt 2 ]; then | |
| usage >&2 | |
| exit 1 | |
| fi | |
| hb_vector=$1 | |
| shift | |
| fontfile=$1 | |
| shift | |
| if [ "x${fontfile:0:1}" = "x-" ]; then | |
| echo "Specify font file before vector options." >&2 | |
| exit 1 | |
| fi | |
| if [ ! -f "$fontfile" ]; then | |
| echo "Font file not found: '$fontfile'." >&2 | |
| exit 1 | |
| fi | |
| if ! echo "$hb_vector" | grep -q 'vector'; then | |
| echo "Specify hb-vector as first positional argument: got '$hb_vector'." >&2 | |
| exit 1 | |
| fi | |
| if [[ "$hb_vector" == */* ]]; then | |
| hb_vector_path="$hb_vector" | |
| else | |
| hb_vector_path="$(command -v "$hb_vector" 2>/dev/null || true)" | |
| fi | |
| if [ -z "${hb_vector_path:-}" ] || [ ! -x "$hb_vector_path" ]; then | |
| echo "hb-vector not found or not executable: '$hb_vector'." >&2 | |
| exit 1 | |
| fi | |
| hb_vector="$hb_vector_path" | |
| tool_dir="$(cd "$(dirname "$hb_vector")" && pwd)" | |
| hb_shape="$tool_dir/hb-shape" | |
| hb_view="$tool_dir/hb-view" | |
| hb_subset="$tool_dir/hb-subset" | |
| for tool in "$hb_shape" "$hb_view" "$hb_subset"; do | |
| if [ ! -x "$tool" ]; then | |
| echo "Required sibling tool not found or not executable: '$tool'." >&2 | |
| exit 1 | |
| fi | |
| done | |
| if [ "x${fontfile:0:1}" = "x/" ]; then | |
| fontfile_path="$fontfile" | |
| else | |
| fontfile_path="$PWD/$fontfile" | |
| fi | |
| fontfile="$fontfile_path" | |
| options=() | |
| shape_view_options=() | |
| have_text=false | |
| text= | |
| for arg in "$@"; do | |
| if ! $have_text && [ "x${arg:0:1}" = "x-" ]; then | |
| if echo "$arg" | grep -q ' '; then | |
| echo "Space in argument is not supported: '$arg'." >&2 | |
| exit 1 | |
| fi | |
| options+=("$arg") | |
| if ! is_vector_only_option "$arg"; then | |
| shape_view_options+=("$arg") | |
| fi | |
| continue | |
| fi | |
| if $have_text; then | |
| echo "Too many arguments found. Use '=' notation for options, eg --font-size=64." >&2 | |
| exit 1 | |
| fi | |
| text=$arg | |
| have_text=true | |
| done | |
| options_len=${#options[@]} | |
| shape_view_options_len=${#shape_view_options[@]} | |
| if ! $have_text; then | |
| text=$(cat) | |
| fi | |
| if [[ "$text" == *';'* ]]; then | |
| echo "Text cannot contain ';' in .tests format." >&2 | |
| exit 1 | |
| fi | |
| if [[ "$text" == *$'\n'* ]]; then | |
| echo "Multiline text is not supported by .tests format." >&2 | |
| exit 1 | |
| fi | |
| if [[ "$test_name" == *';'* ]]; then | |
| echo "Test name cannot contain ';'." >&2 | |
| exit 1 | |
| fi | |
| if [[ "$test_name" == *$'\n'* ]]; then | |
| echo "Test name cannot contain newlines." >&2 | |
| exit 1 | |
| fi | |
| unicodes=$(/usr/bin/env python3 -c 'import sys; s=sys.argv[1]; print(",".join(f"U+{ord(c):04X}" for c in s))' "$text") | |
| options_str= | |
| if [ "$options_len" -gt 0 ]; then | |
| for arg in "${options[@]}"; do | |
| options_str="${options_str}${options_str:+ }${arg}" | |
| done | |
| fi | |
| ext=svg | |
| if [ "$options_len" -gt 0 ]; then | |
| for arg in "${options[@]}"; do | |
| if [ "x$arg" = "x-q" ] || [ "x$arg" = "x--quiet" ]; then | |
| ext=txt | |
| break | |
| fi | |
| done | |
| fi | |
| if [ "$out" = "/dev/stdout" ] || [ "$out" = "-" ]; then | |
| out_dir=$PWD | |
| else | |
| mkdir -p "$(dirname "$out")" | |
| out_dir=$(cd "$(dirname "$out")" && pwd) | |
| fi | |
| tmpdir=$(mktemp -d) | |
| # Collect gids from shaping result and subset. | |
| if [ "$shape_view_options_len" -gt 0 ]; then | |
| glyph_ids=$( | |
| printf '%s\n' "$text" | "$hb_shape" "${shape_view_options[@]}" --no-glyph-names --no-clusters --no-positions "$fontfile" | | |
| sed 's/[][]//g; s/|/,/g' | |
| ) | |
| else | |
| glyph_ids=$( | |
| printf '%s\n' "$text" | "$hb_shape" --no-glyph-names --no-clusters --no-positions "$fontfile" | | |
| sed 's/[][]//g; s/|/,/g' | |
| ) | |
| fi | |
| cp "$fontfile" "$tmpdir/font.ttf" | |
| "$hb_subset" \ | |
| --glyph-names \ | |
| --no-hinting \ | |
| --layout-features='*' \ | |
| --gids="$glyph_ids" \ | |
| --text="$text" \ | |
| --output-file="$tmpdir/font.subset.ttf" \ | |
| "$tmpdir/font.ttf" | |
| if ! test -s "$tmpdir/font.subset.ttf"; then | |
| echo "Subsetter did not produce nonempty subset font in $tmpdir/font.subset.ttf." >&2 | |
| exit 2 | |
| fi | |
| # Verify visual parity via hb-view. | |
| if [ "$shape_view_options_len" -gt 0 ]; then | |
| printf '%s\n' "$text" | "$hb_view" "${shape_view_options[@]}" "$fontfile" --output-format=png --output-file="$tmpdir/orig.png" | |
| printf '%s\n' "$text" | "$hb_view" "${shape_view_options[@]}" "$tmpdir/font.subset.ttf" --output-format=png --output-file="$tmpdir/subset.png" | |
| else | |
| printf '%s\n' "$text" | "$hb_view" "$fontfile" --output-format=png --output-file="$tmpdir/orig.png" | |
| printf '%s\n' "$text" | "$hb_view" "$tmpdir/font.subset.ttf" --output-format=png --output-file="$tmpdir/subset.png" | |
| fi | |
| if ! cmp -s "$tmpdir/orig.png" "$tmpdir/subset.png"; then | |
| echo "Subset font produced different rendering. Please inspect $tmpdir/orig.png and $tmpdir/subset.png." >&2 | |
| exit 2 | |
| fi | |
| if [ "x${subset_dir:0:1}" = "x/" ]; then | |
| subset_dir_path="$subset_dir" | |
| else | |
| subset_dir_path="$out_dir/$subset_dir" | |
| fi | |
| mkdir -p "$subset_dir_path" | |
| subset_hash=$(sha1_file "$tmpdir/font.subset.ttf") | |
| subset_path="$subset_dir_path/$subset_hash.ttf" | |
| if ! test -f "$subset_path"; then | |
| mv "$tmpdir/font.subset.ttf" "$subset_path" | |
| fi | |
| if [ -z "$expected_file" ]; then | |
| if [ -z "$test_name" ]; then | |
| echo "Specify --test-name NAME (or --expected-file PATH)." >&2 | |
| exit 1 | |
| fi | |
| if ! $expected_dir_explicit && [ "$out" != "/dev/stdout" ] && [ "$out" != "-" ]; then | |
| tests_base="$(basename "$out")" | |
| tests_stem="${tests_base%.tests}" | |
| if [ -z "$tests_stem" ]; then | |
| tests_stem="$tests_base" | |
| fi | |
| expected_dir="$expected_dir/$tests_stem" | |
| fi | |
| expected_file="$expected_dir/$test_name.$ext" | |
| fi | |
| if [ "x${expected_file:0:1}" = "x/" ]; then | |
| expected_path="$expected_file" | |
| else | |
| expected_path="$out_dir/$expected_file" | |
| fi | |
| mkdir -p "$(dirname "$expected_path")" | |
| if [ "$options_len" -gt 0 ]; then | |
| "$hb_vector" "${options[@]}" "$subset_path" "$text" > "$expected_path" | |
| else | |
| "$hb_vector" "$subset_path" "$text" > "$expected_path" | |
| fi | |
| if [ "$out" = "/dev/stdout" ] || [ "$out" = "-" ]; then | |
| font_field=$(/usr/bin/env python3 -c 'import os,sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))' "$subset_path" "$PWD") | |
| expected_field=$(/usr/bin/env python3 -c 'import os,sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))' "$expected_path" "$PWD") | |
| out_file=/dev/stdout | |
| else | |
| font_field=$(/usr/bin/env python3 -c 'import os,sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))' "$subset_path" "$out_dir") | |
| expected_field=$(/usr/bin/env python3 -c 'import os,sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))' "$expected_path" "$out_dir") | |
| out_file="$out" | |
| fi | |
| echo "${font_field};${options_str};${unicodes};${expected_field}" >> "$out_file" | |
| rm -rf "$tmpdir" |