Created
March 4, 2025 16:06
-
-
Save steveroush/528f9fde1433d382afd329c2cd0f9d0e to your computer and use it in GitHub Desktop.
improveSVG.sh - to improve SVG files created by Graphviz
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
#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