#!@l_prefix@/bin/perl ## ## fontface -- Font Conversion Utility for Web usage via CSS @font-face ## Copyright (c) 2010-2015 Ralf S. Engelschall ## Licensed under GPL ## fontface.pl: Tool Implementation (language: Perl/5) ## # external requirements use IO::All; use Getopt::Long; use POSIX; # parse command line arguments my $opt = { -trace => 0, -verbose => 0, -outputdir => ".", -outputformat => "ttf,eot,woff,svgz,css,html", -unicode => "0000-00FF", -scale => 0, -simplify => 0, -round => 0, -autohint => 0, -autoinst => 0, -nokerning => 0, -nolocal => 0, -family => "", -fullfamily => 0, -prefix => "", -forceweight => "", -forcestyle => "", -forcestretch => "", -forcevariant => "", -specimen => '@l_prefix@/share/fontface/fontface.html', -fontforge => '@l_prefix@/bin/fontforge', -fontfixname => '@l_prefix@/bin/font-fixname', -fontmodname => '@l_prefix@/bin/font-modify-names', -ttf2eot => '@l_prefix@/bin/ttf2eot', -ttf2woff => '@l_prefix@/bin/ttf2woff', -ttfautohint => '@l_prefix@/bin/ttfautohint', -woff2compress => '@l_prefix@/bin/woff2_compress', -gzip => '@l_prefix@/bin/gzip' }; my $p = new Getopt::Long::Parser; $p->configure("bundling"); $p->configure("require_order"); $p->getoptions( "t|trace" => \$opt->{-trace}, "v|verbose" => \$opt->{-verbose}, "o|outputdir=s" => \$opt->{-outputdir}, "O|outputformat=s" => \$opt->{-outputformat}, "u|unicode=s" => \$opt->{-unicode}, "x|scale=i" => \$opt->{-scale}, "s|simplify=i" => \$opt->{-simplify}, "r|round" => \$opt->{-round}, "h|autohint" => \$opt->{-autohint}, "i|autoinst" => \$opt->{-autoinst}, "k|nokerning" => \$opt->{-nokerning}, "l|nolocal" => \$opt->{-nolocal}, "f|family=s" => \$opt->{-family}, "F|fullfamily" => \$opt->{-fullfamily}, "p|prefix=s" => \$opt->{-prefix}, "W|force-weight=s" => \$opt->{-forceweight}, "Y|force-style=s" => \$opt->{-forcestyle}, "S|force-stretch=s" => \$opt->{-forcestretch}, "V|force-variant=s" => \$opt->{-forcevariant}, "specimen=s" => \$opt->{-specimen}, "fontforge=s" => \$opt->{-fontforge}, "fontfixname=s" => \$opt->{-fontfixname}, "fontmodname=s" => \$opt->{-fontmodname}, "ttf2eot=s" => \$opt->{-ttf2eot}, "ttf2woff=s" => \$opt->{-ttf2woff}, "ttfautohint=s" => \$opt->{-ttfautohint}, "woff2compress=s" => \$opt->{-woff2compress}, "gzip=s" => \$opt->{-gzip} ) or die "option parsing failed"; my $file = $ARGV[0] or die "no input font given"; # helper function for running an external command sub run { my ($cmd) = @_; if ($opt->{-trace}) { print "\$ $cmd\n"; } my $rc = system($cmd); if ($rc != 0) { print STDERR "$0: ERROR: command failed with non-zero result code\n"; exit(1); } } # helper function for displaying verbose output sub verbose { my ($msg) = @_; if ($opt->{-verbose}) { print "++ $msg\n"; } } # generate output filename sub outfile ($$) { my ($source, $ext) = @_; my $target = $source; $target =~ s/^([^\/]+)$/.\/$1/s; if ($opt->{-prefix} ne "") { $target =~ s/^.*\//$opt->{-outputdir}\/$opt->{-prefix}-/s; } else { $target =~ s/^.*\//$opt->{-outputdir}\//s; } $target =~ s/\.[^.]+$/.$ext/s; return $target; } # determine output formats my $formats = { -ttf => 0, -eot => 0, -woff => 0, -woff2 => 0, -svgz => 0, -css => 0, -html => 0 }; foreach my $format (split(/,/, $opt->{-outputformat})) { if ($format =~ m/^(?:ttf|eot|woff|woff2|svgz|css|html)$/) { $formats->{"-$format"} = 1; } else { print STDERR "$0: ERROR: invalid output format \"$format\"\n"; exit(1); } } if (not $formats->{-css} and $formats->{-html}) { print STDERR "$0: ERROR: HTML output format requires also CSS output format\n"; exit(1); } # convert into TTF if ($formats->{-ttf}) { verbose("generating TTF format"); } else { verbose("temporarily generating TTF format"); } my $ttf = outfile($file, "ttf"); my $script = "Open(\$1,1);" . "if(\$iscid);CIDFlatten();endif;" . "Reencode(\"unicode\");" . "SelectNone();" . "SelectMoreSingletons(0u20);"; foreach my $range (split(/,/, $opt->{-unicode})) { if ($range =~ m/^([\da-fA-F]+)-([\da-fA-F]+)$/) { $script .= sprintf("SelectMore(0u%s,0u%s);", $1, $2); } elsif ($range =~ m/^([\da-fA-F]+)$/) { $script .= sprintf("SelectMoreSingletons(0u%s);", $range); } else { print STDERR "$0: ERROR: invalid Unicode range \"$range\"\n"; exit(1); } } $script .= "SelectInvert();" . "DetachAndRemoveGlyphs();" . "SelectAll();" . ($opt->{-scale} > 0 ? sprintf("ScaleToEm(%d);", $opt->{-scale}) : "") . ($opt->{-simplify} > 0 ? sprintf("Simplify(1|2|4|8|32|64,%d);", $opt->{-simplify}) : "") . ($opt->{-round} ? "RoundToInt();" : "") . ($opt->{-nokerning} ? "RemoveAllKerns();" : "") . ($opt->{-autoinst} ? "AutoInst();" : "") . "SetOS2Value(\"FSType\", 0);" . "Generate(\$2);" . "Close()"; run("$opt->{-fontforge} -c '$script' $file $ttf 2>/dev/null"); run("$opt->{-fontfixname} $ttf $ttf.new && mv $ttf.new $ttf"); # optionally override font family name and prefix all names if ($opt->{-family} ne "" or $opt->{-prefix} ne "") { my $flags = ""; if ($opt->{-family} ne "") { foreach my $name (qw(family preferred-family wws-family)) { $flags .= " --set $name \"" . $opt->{-family} . "\""; } } if ($opt->{-prefix} ne "") { foreach my $name (qw(family full-name preferred-family wws-family)) { $flags .= " --prepend $name \"" . $opt->{-prefix} . " \""; } $flags .= " --prepend postscript \"" . $opt->{-prefix} . "-\""; } run("$opt->{-fontmodname} $flags $ttf $ttf.new 2>/dev/null && mv $ttf.new $ttf"); } # optionally autohint for small-size rendering if ($opt->{-autohint}) { verbose("autohinting TTF format"); run("$opt->{-ttfautohint} -n $ttf $ttf.new && mv $ttf.new $ttf"); } # convert into EOT my $eot; if ($formats->{-eot}) { verbose("generating EOT format"); $eot = outfile($file, "eot"); run("$opt->{-ttf2eot} <$ttf >$eot"); } # convert into WOFF my $woff; if ($formats->{-woff}) { verbose("generating WOFF format"); $woff = outfile($file, "woff"); run("$opt->{-fontforge} -c 'Open(\$1);Generate(\$2);Close()' $ttf $woff 2>/dev/null"); run("$opt->{-ttf2woff} -i -O -t woff $woff 2>/dev/null"); } # convert into WOFF2 my $woff2; if ($formats->{-woff2}) { verbose("generating WOFF2 format"); $woff2 = outfile($file, "woff2"); run("$opt->{-woff2compress} $ttf $woff2 >/dev/null"); } # generate corresponding CSS snippet my $font_family = ''; my $font_weight = 'normal'; my $font_style = 'normal'; my $font_stretch = 'normal'; my $font_variant = 'normal'; my $font_svgid = ''; # convert into SVGZ my $svg = ""; if ($formats->{-svgz} or $formats->{-css}) { if ($formats->{-svgz}) { verbose("generating SVGZ format"); } else { verbose("temporarily generating SVGZ format"); } $svg = outfile($file, "svg"); run("$opt->{-fontforge} -c 'Open(\$1);Generate(\$2);Close()' $ttf $svg 2>/dev/null"); # determine font family/weight/style/stretch via SVG if ($formats->{-css}) { verbose("determine font information via SVGZ format"); my $xml < io($svg); if ($xml =~ m/\s+font-family="(.+?)"/s) { $font_family = $1; } if ($xml =~ m/\s+font-weight="(.+?)"/s) { $font_weight = $1; } if ($xml =~ m/\s+font-style="(.+?)"/s) { $font_style = $1; } if ($xml =~ m/\s+font-stretch="(.+?)"/s) { $font_stretch = $1; } if ($xml =~ m/\s+font-variant="(.+?)"/s) { $font_variant = $1; } if ($xml =~ m/{-svgz}) { # compress SVG run("$opt->{-gzip} -9 <$svg >${svg}z; rm -f $svg"); $svg = $svg . "z"; } else { verbose("removing temporarily generated SVGZ format"); run("rm -f $svg"); } } # start generating CSS text snippet if ($formats->{-css}) { # fix weight if ($font_weight =~ m/^\d+$/) { # While Firefox, Internet Explorer and Opera have no problem # with "font-weight" which is not a multiple of 100, OmniWeb, # Safari and Chrome dislike it and seem to treat the font then # incorrectly. So, always round down to the next multiple of 100. if ((0+$font_weight) % 100 != 0) { if ((0+$font_weight) > 500) { $font_weight = "" . (POSIX::ceil($font_weight / 100) * 100); } else { $font_weight = "" . (POSIX::floor($font_weight / 100) * 100); } } # translate weight to names which have a fixed mapping if ($font_weight == 400) { $font_weight = "normal"; } elsif ($font_weight == 700) { $font_weight = "bold"; } } # optionally let the font discriminating parameters (below same family) be forced if ($opt->{-forceweight} ne "") { $font_weight = $opt->{-forceweight}; } if ($opt->{-forcestyle} ne "") { $font_style = $opt->{-forcestyle}; } if ($opt->{-forcestretch} ne "") { $font_stretch = $opt->{-forcestretch}; } if ($opt->{-forcevariant} ne "") { $font_variant = $opt->{-forcevariant}; } # determine font name and version via explicit query my $fontforge = {}; my @vars = qw(fullname fontname); my $cmd = "$opt->{-fontforge} -c 'Open(\$1);"; foreach my $var (@vars) { $cmd .= ";Print(\$$var)"; } $cmd .= ";Close()' $ttf 2>/dev/null"; my $output = `$cmd`; my @output = split(/\r?\n/, $output); my $i = 0; foreach my $var (@vars) { $fontforge->{$var} = $output[$i++] }; my $font_fullname = $fontforge->{"fullname"}; my $font_fontname = $fontforge->{"fontname"}; # optionally make the "font-family" include all other "font-*" # information (this is necessary to workaround Opera and IE bugs which # require unique "font-family" when loading multiple fonts of the same # family) we have to keep the name extension very short as Internet # Explorer ignores all fonts with a name longer than 32 characters. if ($opt->{-fullfamily}) { my $id = ""; $id .= uc(substr($font_style, 0, 1)); $id .= uc(substr($font_weight, 0, 1)); $id .= uc(substr($font_stretch, 0, 1)); $id .= uc(substr($font_variant, 0, 1)); $font_family .= " " . $id; } # generate CSS text snippet verbose("generating CSS text snippet"); my $woff_name = $woff; $woff_name =~ s/^.*\///; my $woff2_name = $woff2; $woff2_name =~ s/^.*\///; my $eot_name = $eot; $eot_name =~ s/^.*\///; my $ttf_name = $ttf; $ttf_name =~ s/^.*\///; my $svg_name = $svg; $svg_name =~ s/^.*\///; my $txt = ""; $txt .= "\n" . "/* $font_fullname */\n" . "\@font-face {\n" . " font-family: '$font_family';\n" . " src: "; if ($formats->{-eot}) { $txt .= "url('$eot_name');\n"; $txt .= " src: "; } if (not $opt->{-nolocal}) { $txt .= "local('$font_fullname'),\n"; $txt .= " local('$font_fontname')" if ($font_fullname ne $font_fontname); } elsif ($formats->{-eot}) { $txt .= "local('*'),\n"; $txt .= " url('$eot_name?#iefix') format('embedded-opentype')"; } if ($formats->{-woff2}) { $txt .= ",\n " if ($txt !~ m/\s+$/); $txt .= "url('$woff2_name') format('woff2')"; } if ($formats->{-woff}) { $txt .= ",\n " if ($txt !~ m/\s+$/); $txt .= "url('$woff_name') format('woff')"; } if ($formats->{-ttf}) { $txt .= ",\n " if ($txt !~ m/\s+$/); $txt .= "url('$ttf_name') format('truetype')"; } if ($formats->{-svgz}) { $txt .= ",\n " if ($txt !~ m/\s+$/); $txt .= "url('$svg_name#$font_svgid') format('svg')"; } $txt .= ";\n"; $txt .= " font-style: $font_style;\n" . " font-weight: $font_weight;\n" . " font-stretch: $font_stretch;\n" . " font-variant: $font_variant;\n" . "}\n" . "\n"; my $css = outfile($file, "css"); $txt > io($css); print $txt if ($opt->{-verbose}); # generate HTML specimen page if ($formats->{-html}) { verbose("generating HTML specimen page"); my $specimen = outfile($file, "html"); my $html < io($opt->{-specimen}); my $xxx = $css; $xxx =~ s/^.*\/([^\/]+)$/$1/s; $html =~ s/\@font-cssfile\@/$xxx/sg; $html =~ s/\@font-fullname\@/$font_fullname/sg; $html =~ s/\@font-fontname\@/$font_fontname/sg; $html =~ s/\@font-family\@/$font_family/sg; $html =~ s/\@font-style\@/$font_style/sg; $html =~ s/\@font-weight\@/$font_weight/sg; $html =~ s/\@font-stretch\@/$font_stretch/sg; $html =~ s/\@font-variant\@/$font_variant/sg; $html > io($specimen); } } # final post-processing if (not $formats->{-ttf}) { verbose("removing temporarily generated TTF format"); run("rm -f $ttf"); }