Skip to content

Instantly share code, notes, and snippets.

@steveroush
Created March 4, 2025 16:06
Show Gist options
  • Save steveroush/528f9fde1433d382afd329c2cd0f9d0e to your computer and use it in GitHub Desktop.
Save steveroush/528f9fde1433d382afd329c2cd0f9d0e to your computer and use it in GitHub Desktop.
improveSVG.sh - to improve SVG files created by Graphviz
#set -x
:<<"__COMMENT__"
improveSVG.sh -
a program to improve the font usage / text placement in SVG files created by Graphviz programs.
usage:
# if the resulting SVG file will only be used on this computer
bash improveSVG.sh myFile.gv > myFile.svg
# if the resulting SVG file will be shared (creates a large (500,000+ bytes) SVG file
bash improveSVG.sh -E myFile.gv > myFile.svg
# if the resulting SVG file will be shared (creates a smaller SVG file)
# (requires installing fonttools programs)
bash improveSVG.sh -ES myFile.gv > myFile.svg
########################################################################################
we need a clear relationship between the font file used for sizing and the
font-family specified in the svg output file
the "resolved" line in stderr when using the "-v" command line option does not always
provide the font-family value in the svg output
(e.g. if "ZapfChancery-MediumItalic" is the starting font name)
so, we will
- extract each of the fontnames & fontfiles used by the input file (explicitly & implicitly), (from stderr)
- add fontnames=ps - to get a useable version of the fontname in the "text" svg lines
-
then
we will run the layout engine to produce the svg (with fontnames=ps)
and edit the svg
- embedding or linking to the font files
- modifying the "<text" lines to change the font-family value and
- removing any existing font-weight, font-stretch, or font-style descriptors
- adding font-weight, font-stretch, and font-style descriptors with correct values for SVG 1.1
########################################################################################
__COMMENT__
( set -- `ps -p $$`;N=$#;
eval "S=\$$N"
if [ "$S" != "bash" ];then
echo "Warning: you are using \"$S\", \"bash\" may work better" >&2
fi
)
E=dot
#E=/usr/bin/dot
T=svg
Embed=0
Subset=0
export Style Stretch Weight
usage() { # Function: Print a help message.
echo "Usage: $0 [ -E ] [ -S ] myFile.gv" 1>&2
}
exit_abnormal() { # Function: Exit with error.
usage
exit 1
}
while getopts "SE" options; do
case "${options}" in
E)
Embed=1
which -s base64 || { echo "Error: you must install base64" >&2 ;exit 1; }
;;
S)
Subset=1
which -s pyftsubset || { echo "Error: you must install fonttools" >&2 ;exit 1; }
;;
:) # If expected argument omitted:
echo "Error: -${OPTARG} requires an argument."
exit_abnormal # Exit abnormally.
;;
*) # If unknown (any other) option:
exit_abnormal # Exit abnormally.
;;
esac
done
shift $(($OPTIND - 1))
f=$1
F=${f%.*}
E=dot
StdErr=/tmp/err.$F.errs
StdOut=/tmp/out$F.$T
#set -x
## make sure we use fontnames=ps
$E -v -Gfontnames=ps -T$T "$f" -o$StdOut 2>$StdErr
#set -x
gawk -F '"' -v "Embed=$Embed" -v "Subset=$Subset" -v "dotFile=$f" '
function printXmlComment (aString){
print "\n<!-- " aString " -->\n"
#print aString > "/dev/stderr"
}
function bufferXmlComment (aString){
gsub(/[-<]/,"*",aString)
Buffer=Buffer "\n<!-- " aString " -->"
#print aString > "/dev/stderr"
}
function resolved2css(resName){
retS=""
bufferXmlComment(" resolved2css: " resName " >" tok[i] "<")
args=tolower(resName)
#sub(/[^,]*,/,"",args)
bufferXmlComment(" after sub: >" args "<")
delete(tok)
cnt=split(args, tok, "[ ,] *")
for (i=2;i<=cnt;i++){ # start at 2, skip the name
bufferXmlComment(" " resName " >" tok[i] "<")
if (Arg[tok[i]] != "")
retS=retS " " Arg[tok[i]]
}
bufferXmlComment(" " resName " returns >" retS "<")
return retS
}
BEGIN{
first = " <defs>\n <style>"
last = " </style>\n </defs>"
Arg["thin"] = "font-weight=\"100\""
Arg["extralight"] = "font-weight=\"200\""
Arg["light"] = "font-weight=\"300\""
Arg["ormal"] = "font-weight=\"400\""
Arg["medium"] = "font-weight=\"500\""
Arg["semibold"] = "font-weight=\"600\""
Arg["demibold"] = "font-weight=\"600\""
Arg["bold"] = "font-weight=\"700\""
Arg["extrabold"] = "font-weight=\"800\""
Arg["ultrabold"] = "font-weight=\"800\""
Arg["black"] = "font-weight=\"900\""
Arg["heavy"] = "font-weight=\"900\""
Arg["extrablack"] = "font-weight=\"900\""
Arg["ultrablack"] = "font-weight=\"900\""
Arg["bold"] = "font-weight=\"bold\""
Arg["book"] = "font-weight=\"300\"" # ???
Arg["normal"] = "font-stretch=\"normal\""
Arg["ultra-condensed"] = "font-stretch=\"ultra-condensed\""
Arg["extra-condensed"] = "font-stretch=\"extra-condensed\""
Arg["condensed"] = "font-stretch=\"condensed\""
Arg["semi-condensed"] = "font-stretch=\"semi-condensed\""
Arg["normal"] = "font-stretch=\"normal\""
Arg["semi-expanded"] = "font-stretch=\"semi-expanded\""
Arg["expanded"] = "font-stretch=\"expanded\""
Arg["extra-expanded"] = "font-stretch=\"extra-expanded\""
Arg["ultra-expanded"] = "font-stretch=\"ultra-expanded\""
Arg["roman"] = "font-style=\"normal\""
Arg["italic"] = "font-style=\"italic\""
Arg["oblique"] = "font-style=\"oblique\""
SQ = "\047" ## "\047" is the single quote character
SP=" "
DQ="\""
}
/resolved to/{
# build array of font info
bufferXmlComment(" RESOLVED: " $0)
origName=tolower($2) # seems to be identical to origName !! use lowercase
resName=$4
resolvedName[origName]=resName
fontFamily[origName]=origName
sub(/,.*/,"",fontFamily[origName])
bufferXmlComment(" family: " fontFamily[origName] " -- " origName)
sub(/^ /,"",$5)
fileName[origName]=$5
args=origName
sub(/,.*/,"",args) ###### what is this
bufferXmlComment(" look " $0)
cssArgs[$4]=resolved2css(origName)
bufferXmlComment(" resArgs " origName " --- " cssArgs[origName])
next
}
#/^(<.xml |<svg )/ {
FNR!=NR && FNR==1{
prt = 1
FS=oFS
needToEmbed=1
}
FNR==NR{next} ## do not print input from stderr
/^<text .*font-family/{
txtLine=$0
#bufferXmlComment(" TEXT " txtLine)
indx=index(txtLine, ">")
while (indx==0){
getline X
txtLine=txtLine " " X
indx=index(txtLine, ">")
}
front=substr(txtLine,1,indx-1)
back=substr(txtLine,indx)
bufferXmlComment(" front--" front "--")
bufferXmlComment(" back--" back "--")
#delete(tok)
cnt=split(front,tok,"\"")
rslt=""
i=1;
while (tok[i]!~/font-family/){
rslt=rslt tok[i++] "\""
}
rslt=rslt tok[i++] "\"" # add the literal string font-family
sub(",.*", "", tok[i])
tok[i]=tolower(tok[i]) ## !! lowercase
bufferXmlComment(" tok[i] --" tok[i] "--" fontFamily[tok[i]])
tok[i]=fontFamily[tok[i]]
if (tok[i]==""){
print "Error: font-family field is now empty" >"/dev/stderr"
print " before:" front back >"/dev/stderr"
}
while (i<cnt){
if(tok[i]~/ font-/)
sub(/font-(style|weight|stretch)/,"old&", tok[i])
rslt=rslt tok[i++] "\""
}
rslt=rslt tok[i]
rslt=rslt " " cssArgs[origName]
bufferXmlComment(" rslt --" rslt "--")
bufferXmlComment(" back --" back "--")
print rslt
print back
next
}
/^<g / && needToEmbed==1 {
keep=$0
print first
for (oName in fileName){
if (fileName[oName] ~ /\.ttf$/) {
FontType = "truetype" ## "ttf" ##
} else if (fileName[oName] ~ /\.otf$/) {
FontType = "opentype" ## "otf" ##
} else if (fileName[oName] ~ /\.woff$/) {
FontType = "woff"
} else if (fileName[oName] ~ /\.woff2$/) {
FontType = "woff2"
}
bufferXmlComment(" file: " oName " -- " fileName[oName])
print " @font-face {"
print " font-family: " SQ fontFamily[oName] SQ ";"
if (Embed==1){
if (Subset==1){
base=fileName[oName]
sub(/.*\//,"",base)
SubsetFile="/tmp/subset_" base
Cmd = "dot -Gphase=1 -Tjson " dotFile
#print Cmd > "/dev/stderr"
while (Cmd | getline >0){
if ($0~/^[ \t]*"text": "/){
sub(/^[ \t]*"text": "/,"");
sub(/"$/,"");
#print $0 > "/dev/stderr"
All=All $0
}
}
#print All > "/tmp/AllChars"
Cmd="pyftsubset \"" fileName[oName] "\" --text-file=/tmp/AllChars --output-file=\"" SubsetFile "\""
print Cmd > "/dev/stderr"
system(Cmd)
fileName[oName]=SubsetFile
}
Cmd = "base64 -w 0 \"" fileName[oName] "\"" # keep all on one line
bufferXmlComment(" base64 cmd: " Cmd)
#print Cmd > "/dev/stderr"
Cmd | getline encoded
#print " src: url(data:application/" FontType ";charset=utf-8;base64," encoded ") format(" SQ FontType SQ ");"
print " src: url(data:application/octet-stream;charset=utf-8;base64," encoded ") format(" SQ FontType SQ ");"
}else{ # linked, not embedded
print " src: url(" SQ fileName[oName] SQ ") format(" SQ FontType SQ ");"
}
print " }"
}
print last
needToEmbed=0
print keep
next
}
/<\/svg>/{
#print Buffer;
Buffer=""
}
{print}
' $StdErr $StdOut
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment