#! /usr/bin/perl
#todo: don't die on every error, try to keep going
use strict;
use warnings;
use Font::TTF::Font;
use XML::Parser::Expat;
use Getopt::Std;
use File::Temp qw(tempfile);
use Compress::Zlib;
#### global variables & constants ####
my $version = "1.2"; #add old_names section to feat_all.xml
#1.1 - change processing order
#$opt_h - help via the usage message
#$opt_d - debug output
#$opt_f - for add & extract subcommands, don't check whether proper element at start of file
#$opt_t - output feat_set.xml file with all settings at non-default values for testing TypeTuner
#$opt_m - maximum length of featset suffix for font name
#$opt_n - string to use a suffix at end of font name instead of featset string
#$opt_o - name for output font file instead of generating by appending _tt
#$opt_v - version number (bypasses adding featset suffix to the version)
#$opt_x - for simplified command line, call createset
#The $opt_? vars are initialized in &cmd_line_exec on each call into the package
#The list of $opt_? vars here MUST match the list in $cmd_line_exec
use vars qw($opt_h $opt_d $opt_f $opt_t $opt_m $opt_n $opt_o $opt_v $opt_x); #set by &getopts
my $opt_str = 'hdftm:n:o:v:x';
my $family_name_id = 1; #source for family name to modify
my $full_font_name_id = 4; #full font name for setmetrics command
my $version_name_id = 5;
my $family_name_ids = [1, 3, 4, 16, 18]; #name ids where family might occur
my $post_family_name_ids = [6];
my $version_name_ids = [5];
my $feat_all_elem = "all_features";
my $feat_set_elem = "features_set";
my $table_nm = "Silt";
my $font_nm_len_limit = 31; #maximum length of font name according to TrueType spec
#### subroutines ####
sub Feat_All_parse($\%\%)
#parse $feat_all_fn to create the $feat_all and $feat_tag structures
#see "TypeTuner_notes.txt" for description of data structures and XML format
{
my ($feat_all_fn, $feat_all, $feat_tag) = @_;
my ($xml_parser, $tag, $tmp, $current, $last);
$xml_parser = XML::Parser::Expat->new();
$xml_parser->setHandlers('Start' => sub {
my ($xml_parser, $elem, %attrs) = @_;
if ($elem eq $feat_all_elem)
{
$feat_all->{'version'} = $attrs{'version'};
}
elsif ($elem eq $feat_set_elem)
{
die("$feat_set_elem XML file provided instead of $feat_all_elem XML file\n");
}
elsif ($elem eq 'features')
{}
elsif ($elem eq 'feature')
{
$tag = $attrs{'tag'};
if (defined $feat_all->{'features'}{$tag} || $tag =~ /-/)
{die("feature tags must be unique and can't contain hyphen(s): $tag\n");}
$feat_all->{'features'}{$tag}{'name'} = $attrs{'name'};
$feat_all->{'features'}{$tag}{'default'} = $attrs{'value'};
if (not defined $feat_all->{'features'}{' tags'})
{$feat_all->{'features'}{' tags'} = [];}
push(@{$feat_all->{'features'}{' tags'}}, $tag);
add_name_tag($feat_tag, $attrs{'name'}, $tag);
$current = $feat_all->{'features'}{$tag}; #'values' to be added
}
elsif ($elem eq 'value')
{
$tag = $attrs{'tag'};
if (defined $current->{'values'}{$tag} || $tag =~ /-/)
{die("for feature $current->{'name'}, value tags must be unique and can't contain hyphen(s): $tag\n");}
$current->{'values'}{$tag}{'name'} = $attrs{'name'};
if (not defined $current->{'values'}{' tags'})
{$current->{'values'}{' tags'} = [];}
push(@{$current->{'values'}{' tags'}}, $tag);
add_name_tag($feat_tag, $attrs{'name'}, $tag);
$last = $current;
$current = $current->{'values'}{$tag}; #'cmds' to be added
}
elsif ($elem eq 'interactions')
{}
elsif ($elem eq 'test')
{
$tmp = {'test' => $attrs{'select'}};
if (not defined $feat_all->{'interactions'})
{$feat_all->{'interactions'} = [];}
push(@{$feat_all->{'interactions'}}, $tmp);
$current = $feat_all->{'interactions'}[-1]; #'cmds' to be added
}
elsif ($elem eq 'cmd_blocks')
{}
elsif ($elem eq 'cmd_block')
{
$tmp = $attrs{'name'};
$feat_all->{'cmd_blocks'}{$tmp} = {}; #'cmds' to be added
$current = $feat_all->{'cmd_blocks'}{$tmp};
}
elsif ($elem eq 'cmd') #features, interactions, cmd_blocks
{
# $current is pointer to a hash
$tmp = {'cmd' => $attrs{'name'}, 'args' => $attrs{'args'}};
if (not defined $current->{'cmds'})
{$current->{'cmds'} = [];} #array of refs to {cmd, args} or {cmd_block}
push(@{$current->{'cmds'}}, $tmp);
}
elsif ($elem eq 'cmds') #features, interactions
{
# $current is pointer to a hash
$tmp = {'cmd_block' => $attrs{'name'}};
if (not defined $current->{'cmds'})
{$current->{'cmds'} = [];} #array of refs to {cmd, args} or {cmd_block}
push(@{$current->{'cmds'}}, $tmp);
}
elsif ($elem eq 'aliases')
{}
elsif ($elem eq 'alias')
{
$tmp = $attrs{'name'};
$feat_all->{'aliases'}{$tmp} = $attrs{'value'};
}
elsif ($elem eq 'old_names')
#the below approach (for old_feature & old_value) adds names to the %feat_tag.
# this seems like the right place to put the info for later lookup.
# the con is that there's no way to tell which feature names come old_names.
# an alternative is to store the info in %feat_all since that contains all info
# from the feat_all.xml file (lookup is just unnecessarily harder).
#must handle cases where the names for feature or value or both change
{}
elsif ($elem eq 'old_feature')
{
add_name_tag($feat_tag, $attrs{'name'}, $attrs{'tag'});
}
elsif ($elem eq 'old_value')
{
#must handle the fact that different features can have the same values
# (the identical value names would have the same tag)
# but only one of the values might change
$tmp = $attrs{'feature'} . '|' . $attrs{'name'}; # '|' is not a likely letter for a feature & value name
add_name_tag($feat_tag, $tmp, $attrs{'tag'});
}
else
{}
}, 'End' => sub {
my ($xml_parser, $elem) = @_;
if ($elem eq 'value')
{
$current = $last;
}
else
{}
}, 'Char' => sub {
my ($xml_parser, $str) = @_;
#die ("XML element content not allowed: $str\n");
});
$xml_parser->parsefile($feat_all_fn) or die "Can't read $feat_all_fn";
}
sub Feat_Set_write($\%)
#write the $feat_set_fn based on the $feat_all structure
{
my ($feat_set_fn, $feat_all) = @_;
my ($feats, $feat_tag, $feat_nm, $feat_val, $val_tag, $val_nm);
open OUT_FILE, ">$feat_set_fn" or die("Could not open $feat_set_fn for writing\n");
print OUT_FILE "\n";
print OUT_FILE "\n";
print OUT_FILE "{'version'}\">\n";
$feats = $feat_all->{'features'};
foreach $feat_tag (@{$feats->{' tags'}})
{
$feat_nm = $feats->{$feat_tag}{'name'};
if (not $opt_t)
{$feat_val = $feats->{$feat_tag}{'default'};}
else #get non-default setting for binary or multi-valued feat
{ foreach (@{$feats->{$feat_tag}{'values'}{' tags'}})
{
$feat_val = $feats->{$feat_tag}{'values'}{$_}{'name'};
if ($feat_val ne $feats->{$feat_tag}{'default'})
{last;}
}
}
print OUT_FILE "\t\n";
foreach $val_tag (@{$feats->{$feat_tag}{'values'}{' tags'}})
{
$val_nm = $feats->{$feat_tag}{'values'}{$val_tag}{'name'};
print OUT_FILE "\t\t\n";
}
print OUT_FILE "\t\n";
}
print OUT_FILE "\n";
}
sub Feat_Set_parse($\%\$\%)
#parse the $feat_set_fn to create the $feat_set string and the %line_metrics hash
{
my ($feat_set_fn, $feat_tag, $feat_set, $line_metrics) = @_;
my ($xml_parser, $tmp, $feature_tag, $value_tag, $feat_set_str);
$feat_set_str = '';
$xml_parser = XML::Parser::Expat->new();
$xml_parser->setHandlers('Start' => sub {
my ($xml_parser, $elem, %attrs) = @_;
if ($elem eq 'feature')
{
$tmp = $attrs{'name'};
$feature_tag = $feat_tag->{$tmp} or die("feature name: $tmp is invalid\n");
$tmp = $attrs{'name'} . '|' . $attrs{'value'};
$value_tag = $feat_tag->{$tmp} || $feat_tag->{$attrs{'value'}}
|| die("feature value: $attrs{'value'} is invalid\n"); # 'or' does NOT work here
$feat_set_str .= "$feature_tag-$value_tag ";
}
elsif ($elem eq $feat_all_elem)
{
die("$feat_all_elem XML file provided instead of $feat_set_elem XML file\n");
}
elsif ($elem eq 'imported_line_metrics')
{
$line_metrics->{'font'} = $attrs{'font'};
$line_metrics->{'em-sqr'} = $attrs{'em-sqr'};
$line_metrics->{'metrics'} = $attrs{'metrics'};
}
else
{}
}, 'End' => sub {
my ($xml_parser, $elem) = @_;
if ($elem eq '')
{}
else
{}
}, 'Char' => sub {
my ($xml_parser, $str) = @_;
#die ("XML element content not allowed: $str\n");
});
$xml_parser->parsefile($feat_set_fn) or die "Can't read $feat_set_fn";
chop $feat_set_str; #remove final space
$$feat_set = $feat_set_str;
}
sub add_name_tag(\%$$)
{
my ($feat_tag, $name, $tag) = @_;
if (defined $feat_tag->{$name}
&& $feat_tag->{$name} ne $tag)
{die("value name: $name mapped to a second different tag: $tag\n");}
$feat_tag->{$name} = $tag;
}
#forward declaration so recursive call won't be flagged as an error
sub copy_cmds(\@\@\%);
sub copy_cmds(\@\@\%)
#copy second array to first array flattening cmd_blocks
#can be called recursively so cmd_blocks can contain cmd_blocks
{
my ($commands, $cmds, $cmd_blocks) = @_;
my ($cmd);
foreach $cmd (@{$cmds}){
if (defined $cmd->{'cmd_block'}) #flatten cmd_blocks
{copy_cmds(@$commands,
@{$cmd_blocks->{$cmd->{'cmd_block'}}{'cmds'}},
%$cmd_blocks);}
else
{push(@{$commands}, $cmd);}}
};
sub sort_tests($$)
#compare to test attribute strings
#sort such that shorter strings come first
{
#scalar split(/\s/, $a) causes many error msgs
my ($a, $b) = @_;
my @t = split(/\s/, $a);
my $a_ct = scalar @t;
@t = split(/\s/, $b);
my $b_ct = scalar @t;
if ($a_ct > $b_ct)
{return 1;}
elsif ($a_ct < $b_ct)
{return -1;}
else #$a_ct == $b_ct
{return ($a cmp $b);}
}
sub Feat_val_tags($)
#extract feature and value tags from a concatenated string containing them together
#returns the tags as a list
{
my ($fv) = @_;
#print "Feat_val_tags fv: '$fv'\n";
if ($fv =~ /(.*)-(.*)/)
{return ($1, $2);}
else
{die("feature-value pair is corrupt: $fv\n");}
}
sub Feat_Set_cmds(\%$\@)
#generate a list of commands (cmd-args hashes) to process based on feature settings
#any cmd_block will be expanded to a list of commands
#first process feature value cmds then interactions tests
#the tests with the fewest elements are processed first
# this way the cmds in tests with the most elements override cmds in tests with fewer elements
{
my ($feat_all, $feat_set, $commands) = @_;
my ($features, $interactions, $cmd_blocks);
$features = $feat_all->{'features'};
$interactions = $feat_all->{'interactions'};
$cmd_blocks = $feat_all->{'cmd_blocks'};
#process feat-value setting based on feature value cmds
my (@feat_val, $feat, $val, $cmds);
@feat_val = split(/\s+/, $feat_set);
foreach my $fv (@feat_val)
{
next if (not $fv);
($feat, $val) = Feat_val_tags($fv);
$cmds = $features->{$feat}{'values'}{$val}{'cmds'};
copy_cmds(@$commands, @$cmds, %$cmd_blocks);
}
#create hash for working with sorted test attributes
my ($interact, %test_str_to_ix, $ix);
$ix = 0;
foreach $interact (@{$interactions})
{$test_str_to_ix{$interact->{'test'}} = $ix++;}
#test feature settings against interaction tests
my ($test_str, @tests, $test, $test_passed);
foreach $test_str (sort sort_tests keys %test_str_to_ix)
{
@tests = split(/\s+/, $test_str);
$test_passed = 1;
foreach $test (@tests)
{ #test if all feat-value settings in an interaction test are set
if (not $feat_set =~ /$test/)
{
$test_passed = 0;
last;
}
}
if ($test_passed)
{ #add to list of commands to process
if ($opt_d) {print "interaction matched: $test_str\n";}
my $cmds = $interactions->[$test_str_to_ix{$test_str}]->{'cmds'};
copy_cmds(@$commands, @$cmds, %$cmd_blocks);
}
else
{
if ($opt_d) {print "interaction not matched: $test_str\n";}
}
}
if ($opt_d) {print "\n";}
};
sub Cmds_exec($\@\%\%)
#execute commands (cmd-args hash) in commands array against the font
#the args string is split into one string for each arg
# Perl handles the conversion from string to number automatically
# where numbers are needed as args in called sub
#args can be surrounded by braces, which means they are looked up in aliases
#args that contain spaces MUST be handled using an alias
{
my ($font, $commands, $feat_all, $line_metrics) = @_;
my ($command, $cmd, $args, @args, $arg);
foreach $command (@$commands)
{
($cmd, $args) = ($command->{'cmd'}, $command->{'args'});
@args = split(/\s+/, $args);
foreach $arg (@args)
{ #handle args in braces
if ($arg =~ /\{(.*)\}/)
{
$arg = $feat_all->{'aliases'}{$1}; #$arg is a ref so this changes @args
if (not defined $arg)
{die("invalid alias: $1\n");}
}
}
if ($cmd eq 'null')
{}
elsif ($cmd eq 'gr_feat')
{
if (scalar @args != 2)
{die ("invalid args for gr_feat cmd: @args\n");}
Gr_feat($font, $args[0], $args[1]);
}
elsif ($cmd eq 'encode')
{
if (scalar @args != 2)
{die ("invalid args for encode cmd: @args\n");}
Encode($font, $args[0], $args[1]);
}
elsif ($cmd eq 'feat_add')
{
if (scalar @args != 5)
{die ("invalid args for feat_add cmd: @args\n");}
Feat_add($font, $args[0], $args[1], $args[2], $args[3], $args[4]);
}
elsif ($cmd eq 'feat_del')
{
if (scalar @args != 4)
{die ("invalid args for feat_del cmd: @args\n");}
Feat_del($font, $args[0], $args[1], $args[2], $args[3]);
}
elsif ($cmd eq 'lookup_add')
{
if (scalar @args != 3)
{die ("invalid args for lookup_add cmd: @args\n");}
Lookup_add($font, $args[0], $args[1], $args[2]);
}
elsif ($cmd eq 'lookup_del')
{
if (scalar @args != 3)
{die ("invalid args for lookup_del cmd: @args\n");}
Lookup_del($font, $args[0], $args[1], $args[2]);
}
elsif ($cmd eq 'line_gap')
{
if (scalar @args != 2)
{die ("invalid args for line_gap cmd: @args\n");}
Line_gap_mod($font, $args[0], $args[1]);
}
elsif ($cmd eq 'line_metrics')
{
if (scalar @args != 8)
{die ("invalid args for line_metric cmd: @args\n");}
Line_metrics_mod($font, $args[0], $args[1], $args[2],
$args[3], $args[4],
$args[5], $args[6], $args[7]);
}
elsif ($cmd eq 'line_metrics_scaled')
{
if ($args[0] ne 'null')
{die ("invalid args for line_metrics_scaled cmd: @args\n");}
Line_metrics_scaled_mod($font, $line_metrics);
}
else
{
print "WARNING - unrecognized cmd: $cmd\n";
}
}
};
sub Font_ids_update($\%$\%)
#update various identifying information in the font based on feature settings
{
my ($font, $feat_all, $feat_set, $feat_tag) = @_;
#create user-readable feature-value tags
my ($feats, @feat_val, $feat, $val, $feat_set_active, $true_tag);
$feats = $feat_all->{'features'};
$feat_set_active = '';
#if there are no binary valued features, a True tag may not exist
$true_tag = defined $feat_tag->{'True'} ? $feat_tag->{'True'} : 'T';
@feat_val = split(/\s+/, $feat_set);
foreach my $fv (@feat_val)
{ #feat_set_active can be empty string if all settings are at defaults
next if (not $fv);
($feat, $val) = Feat_val_tags($fv);
if ($feats->{$feat}{'default'} ne $feats->{$feat}{'values'}{$val}{'name'})
{#concatenate non-default feature value settings
$feat_set_active .= " " if $feat_set_active;
if ($val ne $true_tag)
{$feat_set_active .= $feat . $val;} #remove hyphen
else
{$feat_set_active .= $feat;} #don't display True value
}
}
if ($opt_d) {print "Font_ids_update: feat_set_active = \'$feat_set_active\'\n";}
#modify font name
my ($family_nm_old, $family_nm_new, $version_str_old, $version_str_new);
$family_nm_old = Name_get($font, $family_name_id);
if (length($family_nm_old) >= $font_nm_len_limit)
{ #handle bizarre case where the original font family name is too long
$family_nm_new = $family_nm_old . ' XT';
}
else
{
my $font_nm_suffix_len = $font_nm_len_limit - length($family_nm_old) - 1; #-1 for space
$font_nm_suffix_len = $opt_m ? $opt_m : $font_nm_suffix_len;
if (length($feat_set_active) <= $font_nm_suffix_len || $opt_n)
{
$family_nm_new = $family_nm_old .
($feat_set_active || $opt_n ? ' ' : '') .
($opt_n ? $opt_n : $feat_set_active);
}
else
{
$family_nm_new = $family_nm_old . ' ' .
substr($feat_set_active, 0, $font_nm_suffix_len - 3) . ' XT';
}
}
Name_mod($font, $family_name_ids, $family_nm_old, $family_nm_new);
if (length($family_nm_new) > $font_nm_len_limit)
{print "WARNING - the font name ($family_nm_new) is longer than allowed by the TrueType spec ($font_nm_len_limit).\n";}
#handle name id 6: PS name, which shouldn't contain spaces
$family_nm_old =~ s/ //g;
$family_nm_new =~ s/ //g;
Name_mod($font, $post_family_name_ids, $family_nm_old, $family_nm_new);
#modify version
$version_str_old = Name_get($font, $version_name_id);
if (not $opt_v)
{
$version_str_new = $version_str_old . ($feat_set_active ? ' ; ' : '') .
$feat_set_active;
}
else
{
if ($version_str_old =~ '(.*Version\s+)(\d+\.\d+)(.*)')
{
$version_str_new = $1 . $opt_v . $3;
}
else
{
print "WARNING - the version string ($version_str_old) is invalid and won't be changed.\n";
$version_str_new = $version_str_old;
}
}
Name_mod($font, $version_name_ids, $version_str_old, $version_str_new);
#modify modification date
$font->{'head'}->read;
if ($opt_d) {printf ("old date: %d ", $font->{'head'}->getdate());}
my $time_cur = time();
$font->{'head'}->setdate($time_cur);
if ($opt_d) {printf ("new date: %d\n", $time_cur);}
}
sub Gr_feat($$$)
#modify the Feat table so that the specified setting becomes the default
# for the given feature
{
my ($font, $gr_feat_id, $gr_set_id) = @_;
my ($grfeat_tbl, $feature, $feat_found, $set_found);
$grfeat_tbl = $font->{'Feat'}->read;
#$grfeat_tbl->print;
($feat_found, $set_found) = (0, 0);
foreach $feature (@{$grfeat_tbl->{'features'}})
{
if ($feature->{'feature'} == $gr_feat_id)
{
$feat_found = 1;
if (defined($feature->{'settings'}{$gr_set_id}))
{
if ($opt_d) {print "Gr_feat: feat_id: $gr_feat_id old_default: $feature->{'default'} new_default: $gr_set_id\n";}
$set_found = 1;
$feature->{'default'} = $gr_set_id;
}
last;
}
}
if (not $feat_found)
{die("feature id not found in TTF: feat_id: $gr_feat_id set_id: $gr_set_id\n");}
if (not $set_found)
{die("set id not availabe for feature in TTF: feat_id: $gr_feat_id set_id: $gr_set_id\n");}
}
sub Encode($$$)
#modify the cmap subtables to encode the glyph indicated by ps_nm at usv_str
{
my ($font, $usv_str, $ps_nm) = @_;
my ($post_tbl, $glyph_id);
#lookup $ps_nm in the post table to get $glyph_id
$post_tbl = $font->{'post'}->read;
$glyph_id = $post_tbl->{'STRINGS'}{$ps_nm};
if (not defined $glyph_id)
{die("PostScript name $ps_nm is not defined in the font.")};
#convert USV string (U+0105) to a number (0x0105)
my ($usv);
$usv = hex($usv_str);
#loop thru cmap subtables
my ($cmap_tbl, $cmap_ct, $i, $cmap_subtbl);
$cmap_tbl = $font->{'cmap'}->read;
$cmap_ct = $cmap_tbl->{'Num'};
if ($opt_d) {printf("Encode: ps_nm: %s glyph_id: %d usv: 0x%04x cmap_ct: %d\n", $ps_nm, $glyph_id, $usv, $cmap_ct);}
for ($i = 0; $i < $cmap_ct; ++$i)
{
#lookup $usv and point to $glyph_id
#print "Encode: remapping cmap $i\n";
$cmap_subtbl = $cmap_tbl->{'Tables'}[$i];
#allow creation of new USVs but protect subtables that can't handle large ones
$cmap_subtbl->{'val'}{$usv} = $glyph_id unless $usv > 0xFFFF && $cmap_subtbl->{'Format'} < 8;
}
#handle $usv_str greater than current max char in OS/2
my ($os2_tbl, $max_char);
$os2_tbl = $font->{'OS/2'}->read;
$max_char = $os2_tbl->{'usLastCharIndex'};
if ($usv > $max_char)
{
$os2_tbl->{'usLastCharIndex'} = $usv;
if ($opt_d) {print "Encode: OS/2 table max char adjusted to $usv\n";}
}
#todo: may need to handle Unicode range bits
}
sub Feat_add($$$$$$)
#adds the named feature to the list of features for the given script and lang
#at the given pos
#though order of features should not matter. (order in lookup table does matter.)
{
my ($font, $tbl_type, $script, $lang, $feat, $pos) = @_;
my ($feats);
$feats = Feats_find($font, $tbl_type, $script, $lang);
if ($opt_d) {print "Feat_add $feat: orig feats = @$feats\n";}
foreach ($feats)
{if ($_ eq $feat)
{print "Feat_add: ***feature already exists: tbl_type = $tbl_type script = $script lang = $lang feat = $feat\n"; return;}}
#push(@$feats, $feat); #add element to array
splice(@$feats, $pos, 0, $feat);
if ($opt_d) {print "Feat_add $feat: chng feats = @$feats\n";}
}
sub Feat_del($$$$$)
#deletes the named feature from the list of features for the given script and lang
{
my ($font, $tbl_type, $script, $lang, $feat) = @_;
my ($feats, $ct, $ix, $found);
$feats = Feats_find($font, $tbl_type, $script, $lang);
if ($opt_d) {print "Feat_del $feat: orig feats = @$feats\n";}
$ct = scalar @$feats;
$found = 0;
for ($ix = 0; $ix < $ct; ++$ix)
{
if (@$feats[$ix] eq $feat)
{
splice(@$feats, $ix, 1); #remove element from array
$found = 1;
last;
}
}
if (not $found)
{print "Feat_del: ***feature not found: tbl_type = $tbl_type script = $script lang = $lang feat = $feat\n"; return;}
if ($opt_d) {print "Feat_del $feat: chng feats = @$feats\n";}
}
sub Feats_find($$$$)
#returns reference to array of feature names for a given script and lang
{
my ($font, $tbl_type, $script, $lang) = @_;
if ($tbl_type ne 'GSUB' and $tbl_type ne 'GPOS')
{die("invalid table type: $tbl_type\n")};
my($tbl, $feats, $reftag);
$tbl = $font->{$tbl_type}->read;
$reftag = $tbl->{'SCRIPTS'}{$script}{$lang}{' REFTAG'};
if (not defined $reftag)
{$feats = $tbl->{'SCRIPTS'}{$script}{$lang}{'FEATURES'};}
else
{$feats = $tbl->{'SCRIPTS'}{$script}{$reftag}{'FEATURES'};}
if (not defined $feats)
{die("Feats_find: could not find features: table = $tbl_type script = $script lang = $lang\n")};
return $feats;
}
sub Lookup_add($$$$)
#adds the lookup index to the list of lookups for a given feature
#assumes the lookup indexes are sorted numerically
{
my ($font, $tbl_type, $feat, $lookup) = @_;
my ($lookups, $ct, $ix);
$lookups = Lookups_find($font, $tbl_type, $feat);
if ($opt_d) {print "Lookup_add $lookup: orig lookups = @$lookups\n";}
$ct = scalar @$lookups;
for ($ix = 0; $ix < $ct; $ix++)
{
if (@$lookups[$ix] < $lookup)
{
next;
}
elsif (@$lookups[$ix] == $lookup)
{
print "Lookup_add: ***lookup already exists: tbl_type = $tbl_type feat = $feat lookup = $lookup\n";
return;
}
else
{
splice(@$lookups, $ix, 0, $lookup); #add element to array
last;
}
}
if ($ix == $ct) #$lookup is greater than all in @$lookups
{
push (@$lookups, $lookup)
}
if ($opt_d) {print "Lookup_add $lookup: chng lookups = @$lookups\n";}
}
sub Lookup_del($$$$)
#deletes the lookup index from the list of lookups for the given feature
{
my ($font, $tbl_type, $feat, $lookup) = @_;
my ($lookups, $ct, $ix, $found);
$lookups = Lookups_find($font, $tbl_type, $feat);
if ($opt_d) {print "Lookup_del $lookup: orig lookups = @$lookups\n";}
$ct = scalar @$lookups;
$found = 0;
for ($ix = 0; $ix < $ct; $ix++)
{
if (@$lookups[$ix] == $lookup)
{
splice (@$lookups, $ix, 1); #remove element from array
$found = 1;
last;
}
}
if (not $found)
{print "Lookup_del: ***lookup not found: tbl_type = $tbl_type feat = $feat lookup = $lookup\n"; return;}
if ($opt_d) {print "Lookup_del $lookup: chng lookups = @$lookups\n";}
}
sub Lookups_find($$$)
#returns reference to array of lookup indexes for the given feature
{
my ($font, $tbl_type, $feat) = @_;
if ($tbl_type ne 'GSUB' and $tbl_type ne 'GPOS')
{die("invalid table type: $tbl_type\n")};
my($tbl, $lookups);
$tbl = $font->{$tbl_type}->read;
$lookups = $tbl->{'FEATURES'}{$feat}{'LOOKUPS'};
if (not defined $lookups)
{die("could not find lookups: table = $tbl_type feature = $feat")};
return $lookups;
}
sub Line_gap_get($)
#returns the ascent and descent from the OS/2 table
#desc will be positive
{
my ($font) = @_;
my ($tbl, $asc, $dsc);
$tbl = $font->{'OS/2'}->read;
$asc = $tbl->{'usWinAscent'};
$dsc = $tbl->{'usWinDescent'};
return ($asc, $dsc);
}
sub Line_gap_mod($$$)
#set the various ascent and descent values in OS/2 and hhea tables
#descent should normally be positive
{
my ($font, $asc, $dsc) = @_;
my ($tbl);
$tbl = $font->{'OS/2'}->read;
if ($opt_d) {print "Line_gap_mod: orig asc = $tbl->{'usWinAscent'} dsc = $tbl->{'usWinAscent'}\n";}
$tbl->{'sTypoAscender'} = $asc;
$tbl->{'sTypoDescender'} = $dsc * -1;
$tbl->{'usWinAscent'} = $asc;
$tbl->{'usWinDescent'} = $dsc;
if ($opt_d) {print "Line_gap_mod: chng asc = $tbl->{'usWinAscent'} dsc = $tbl->{'usWinAscent'}\n";}
$tbl = $font->{'hhea'}->read;
$tbl->{'Ascender'} = $asc;
$tbl->{'Descender'} = $dsc * -1;
}
sub Line_metrics_mod($$$$$$$$$)
#set all the line metrics in the O2/2 and hhea table individually
#descents should all normally be positive
{
my ($font, $TypoAsc, $TypoDsc, $TypoGap, $WinAsc, $WinDsc,
$hheaAsc, $hheaDsc, $hheaGap) = @_;
my ($tbl);
$tbl = $font->{'OS/2'}->read;
if ($opt_d)
{
print "Line_metrics_mod orig: ";
print "TypoAsc = $tbl->{'sTypoAscender'} TypoDsc = $tbl->{'sTypoDescender'} TypoGap = $tbl->{'sTypoLineGap'} ";
print "WinAsc = $tbl->{'usWinAscent'} WinDsc = $tbl->{'usWinDescent'} ";
}
$tbl->{'sTypoAscender'} = $TypoAsc;
$tbl->{'sTypoDescender'} = $TypoDsc * -1;
$tbl->{'sTypoLineGap'} = $TypoGap;
$tbl->{'usWinAscent'} = $WinAsc;
$tbl->{'usWinDescent'} = $WinDsc;
$tbl = $font->{'hhea'}->read;
if ($opt_d)
{
print "hheaAsc = $tbl->{'Ascender'} hheaDsc = $tbl->{'Descender'} hheaGap = $tbl->{'LineGap'}\n";
}
$tbl->{'Ascender'} = $hheaAsc;
$tbl->{'Descender'} = $hheaDsc * -1;
$tbl->{'LineGap'} = $hheaGap;
if ($opt_d)
{
$tbl = $font->{'OS/2'};
print "Line_metrics_mod chng: ";
print "TypoAsc = $tbl->{'sTypoAscender'} TypoDsc = $tbl->{'sTypoDescender'} TypoGap = $tbl->{'sTypoLineGap'} ";
print "WinAsc = $tbl->{'usWinAscent'} WinDsc = $tbl->{'usWinDescent'} ";
$tbl = $font->{'hhea'};
print "hheaAsc = $tbl->{'Ascender'} hheaDsc = $tbl->{'Descender'} hheaGap = $tbl->{'LineGap'}\n";
}
}
sub Line_metrics_scaled_mod($$)
#set all the line metrics in the O2/2 and hhea table individually
#descents should all normally be positive
#the line metrics are scaled based the em-sqr they are specified with
# and the em-sqr of the target font
{
my ($font, $line_metrics) = @_;
my (@metrics, $em_sqr, $scale, $TypoAsc, $TypoDsc, $TypoGap, $WinAsc, $WinDsc,
$hheaAsc, $hheaDsc, $hheaGap);
#test %line_metrics (possibly no imported_line_metrics element in feat_set.xml)
if (not defined $line_metrics->{'font'} or not defined $line_metrics->{'em-sqr'}
or not defined $line_metrics->{'metrics'})
{die("ERROR - imported_line_metrics element missing or invalid in Settings file\n *use the setmetrics command*\n")};
@metrics = split(/\s+/, $line_metrics->{'metrics'});
if (scalar @metrics != 8)
{die("ERROR - imported_line_metrics element contains wrong number of metrics\n *use the setmetrics command*\n")};
$em_sqr = $line_metrics->{'em-sqr'};
($TypoAsc, $TypoDsc, $TypoGap, $WinAsc, $WinDsc,
$hheaAsc, $hheaDsc, $hheaGap) = @metrics;
#find scaling factor for line metrics based on $line_metrics->em-sqr and $font's em-sqr
my ($head_tbl) = $font->{'head'}->read;
$scale = $head_tbl->{'unitsPerEm'} / $em_sqr;
if ($opt_d) {print "Line_metrics_scaled_mod: scale = $scale\n"};
#apply scaling factor to line metrics
foreach (\$TypoAsc, \$TypoDsc, \$TypoGap, \$WinAsc, \$WinDsc,
\$hheaAsc, \$hheaDsc, \$hheaGap) {$$_ *= $scale;}
if ($opt_d) {print "Line_metrics_scaled_mod calling Line_metrics_mod\n"};
Line_metrics_mod($font, $TypoAsc, $TypoDsc, $TypoGap, $WinAsc, $WinDsc,
$hheaAsc, $hheaDsc, $hheaGap);
}
sub Name_get($$)
#returns the name for a given name id
{
my ($font, $name_id) = @_;
my ($name_tbl, $name);
$name_tbl = $font->{'name'}->read;
$name = $name_tbl->find_name($name_id);
if (not $name)
{die("could not find name in font for id: $name_id\n")};
return $name;
}
sub Name_mod($\@$$)
#modifies the name for a given name ids
{
my ($font, $name_ids, $old_name, $new_name) = @_;
my ($name_tbl, $nid, $pid, $eid, $lid, $name);
$name_tbl = $font->{'name'}->read;
# foreach $nid (0 .. $#{$name_tbl->{'strings'}})
foreach $nid (@$name_ids)
{
foreach $pid (0 .. $#{$name_tbl->{'strings'}[$nid]})
{
foreach $eid (0 .. $#{$name_tbl->{'strings'}[$nid][$pid]})
{
foreach $lid (keys %{$name_tbl->{'strings'}[$nid][$pid][$eid]})
{
$name = $name_tbl->{'strings'}[$nid][$pid][$eid]{$lid};
if ($name =~ s/$old_name/$new_name/)
{
$name_tbl->{'strings'}[$nid][$pid][$eid]{$lid} = $name;
if ($opt_d) {print "Name_mod: name = $name nid = $nid pid = $pid eid = $eid lid = $lid\n";}
}
}
}
}
}
}
sub Table_extract($$$)
#extract our table from the $font to the specified file name
#$feat_set_test insures that $feat_set_elem is at the start of the data to be extracted
{
my ($font, $fn, $feat_set_test) = @_;
open FEAT, ">$fn";
binmode(FEAT);
if (not defined $font->{$table_nm})
{die("no $table_nm table in font\n");}
else
{
$font->{$table_nm}->read;
my $tmp = Compress::Zlib::memGunzip($font->{$table_nm}{' dat'});
if ($feat_set_test)
{if (not $tmp =~ /$feat_set_elem/)
{die("table $table_nm does not contain $feat_set_elem\n");}}
print FEAT $tmp;
close FEAT;
}
}
sub Table_add($$$)
#add our table to the $font from the specified file
#$feat_all_test insures that $feat_all_elem is at the start of the file
{
my ($font, $fn, $feat_all_test) = @_;
#read the whole feat_all XML file into memory
my($feat_xml, $tmp);
open FEAT, "<$fn" or die "Can't open XML file\n";
binmode(FEAT);
$tmp = read(FEAT, $feat_xml, 1000000) or die "Can't read XML file\n";
if ($tmp == 1000000)
{die("XML file is too big\n");}
#die if $feat_all_fn does not start with , override test with -f switch
if ($feat_all_test)
{if (not $feat_xml =~ /$feat_all_elem/)
{die("XML file does not contain $feat_all_elem\n");}}
#compress the XML before putting in the font table
$tmp = Compress::Zlib::memGzip($feat_xml);
#add our XML table $table_nm to the ttf
#the instance variables were taken from where Font.pm creates its Tables
$font->{$table_nm} = Font::TTF::Table->new(PARENT => $font,
NAME => "$table_nm",
INFILE => 0,
OFFSET => 0,
LENGTH => 0,
CSUM => 0);
$font->{$table_nm}{' dat'} = $tmp;
}
sub Usage_print()
{
print < (create settings xml file from ttf)
TypeTuner (apply settings xml file to ttf)
or TypeTuner [] [files, ...]
switches:
-m specify maximum length of generated font name suffix
-n specify font name suffix instead of using generated one
-o specify output font.ttf file name
commands:
createset feat_set.xml
setmetrics font_old.ttf feat_set.xml
applyset feat_set.xml font.ttf
applyset_xml feat_all.xml feat_set.xml font.ttf
extract font.ttf feat_set.xml
add feat_all.xml font.ttf
delete font.ttf
END
exit();
};
#### main processing ####
sub cmd_line_exec(@)
{
#define these here so they are intialized on each call
my ($font, %feat_all, $feat_set, %feat_tag, @commands, %line_metrics);
my ($feat_all_fn, $feat_set_fn, $font_fn, $font_out_fn);
#the $opt_? vars are declared above as globals
#initialize them here on each call
#the list of $opt_? vars here MUST match the list above!!!
foreach ($opt_h, $opt_d, $opt_f, $opt_t, $opt_m, $opt_n, $opt_o, $opt_v, $opt_x)
{$_ = undef;}
local (@ARGV) = @_; #use 'local' instead of 'my' so &getopts works right
getopts($opt_str); #sets $opt_?'s and removes the switches from @ARGV
if (scalar @ARGV == 0 || $opt_h)
{Usage_print;}
my ($cmd);
$cmd = $ARGV[0];
if (not $cmd =~ /^(createset|applyset|applyset_xml|delete|extract|add)$/)
{ #no subcommands were given, use simplified command line
if (scalar @ARGV == 2)
{
my ($ext1, $ext2);
($ext1, $ext2) = map {lc(substr($_,-3,3))} ($ARGV[0], $ARGV[1]);
if ($ext1 ne 'xml' || $ext2 ne 'ttf')
{Usage_print;}
if ($opt_x)
{ #createset
$cmd = 'createset';
($ARGV[0], $ARGV[1]) = ($ARGV[1], $ARGV[0]); #swap args
unshift (@ARGV, 'createset'); #shift args to correct positions for this cmd
}
else
{ #applyset
$cmd = 'applyset';
unshift (@ARGV, 'applyset');
}
}
}
if ($cmd eq 'createset')
{ #create feat_set from feat_all either embedded in font or in separate XML file
if (scalar @ARGV != 3)
{Usage_print;}
if ($opt_d) {print "creating feat_set XML file from font\n";}
my ($fn, $ext, $flag, $fh);
$fn = $ARGV[1];
$ext = lc(substr($ARGV[1], -3, 3));
$flag = 0;
if ($ext eq 'ttf') #set $feat_all_fn
{ #extract XML from font into a temp file
$font = Font::TTF::Font->open($fn) or die "Can't open font";
$flag = 1;
($fh, $feat_all_fn) = tempfile(); $fh->close;
if ($opt_d) {print "feat_all_fn: $feat_all_fn\n"}
#$feat_all_fn = substr($fn, 0, -4) . "_feat_all.xml";
Table_extract($font, $feat_all_fn, 0);
$font->release;
}
elsif ($ext eq 'xml')
{
$feat_all_fn = $fn
}
else
{Usage_print;}
$feat_set_fn = $ARGV[2];
Feat_All_parse($feat_all_fn, %feat_all, %feat_tag);
Feat_Set_write($feat_set_fn, %feat_all);
if ($flag)
{unlink($feat_all_fn);}
}
elsif ($cmd eq 'setmetrics')
{ #import line metrics from a legacy font into the feat_set.xml file
if (scalar @ARGV != 3)
{Usage_print;}
if ($opt_d) {print "setting line metrics in feat_set XML file from font\n";}
($font_fn, $feat_set_fn) = ($ARGV[1], $ARGV[2]);
my($line_metric_str, $font_nm, $em_sqr, $tbl);
$font = Font::TTF::Font->open($font_fn) or die "Can't open font\n";
$font_nm = Name_get($font, $full_font_name_id);
$tbl = $font->{'head'}->read;
$em_sqr = $tbl->{'unitsPerEm'};
$tbl = $font->{'OS/2'}->read;
$line_metric_str = "\t{'sTypoAscender'} " . $tbl->{'sTypoDescender'} * -1;
$line_metric_str .= " $tbl->{'sTypoLineGap'} ";
$line_metric_str .= "$tbl->{'usWinAscent'} $tbl->{'usWinDescent'} ";
$tbl = $font->{'hhea'}->read;
$line_metric_str .= "$tbl->{'Ascender'} " . $tbl->{'Descender'} * -1 . " $tbl->{'LineGap'}\"/>\n";
if ($opt_d) {print "line_metric_str = $line_metric_str";} #already contains \n
open FILE, "+<$feat_set_fn" or die("Could not open $feat_set_fn\n");
my @feat_data;
while (not eof) {push(@feat_data, )};
my ($line_spacing_found, $imported_found, $imported_line_metrics_found) = (0, 0, 0);
foreach my $i (0 .. (scalar @feat_data + 1)) # + 1 because two elements could be added to array
{
last if (not defined $feat_data[$i]); # exit early if two elements aren't added to array
#set the "Line spacing" feature to the "Imported" setting
if ($feat_data[$i] =~ s/(^.*.*$)/$1Imported$3/)
{
if ($opt_d) {print "Line spacing changed to Imported\n";}
$line_spacing_found = 1;
}
#add the "Imported" settings if needed
if ($line_spacing_found)
{
if ($feat_data[$i] =~ //)
{$imported_found = 1;}
if ($feat_data[$i] =~ /<\/feature>/)
{
if (not $imported_found)
{
splice(@feat_data, $i, 0, "\t\t\n");
if ($opt_d) {print "Imported setting added\n";}
}
$line_spacing_found = 0;
}
}
#replace the imported_line_metrics element
if ($feat_data[$i] =~ // && not $imported_line_metrics_found)
{
splice(@feat_data, $i, 0, $line_metric_str);
$imported_line_metrics_found = 1;
if ($opt_d) {print "Imported_line_metrics element added\n";}
}
}
if ($opt_o)
{close FILE; open FILE, ">$opt_o" or die("Could not open $opt_o\n");}
else
{seek(FILE, 0, 0);}
foreach (@feat_data) {print FILE $_};
close FILE;
}
elsif ($cmd eq 'applyset' || $cmd eq 'applyset_xml')
{ #apply feat_set to font based on feat_all either embedded in font or in separate XML file
if (scalar @ARGV != 3 && scalar @ARGV != 4)
{Usage_print;}
if ($opt_d) {print "applying feat_set XML file to font\n";}
my ($flag) = 0;
if ($cmd eq 'applyset')
{
($feat_set_fn, $font_fn) = ($ARGV[1], $ARGV[2]);
my ($fh);
#extract XML from font into a temp file
$font = Font::TTF::Font->open($font_fn) or die "Can't open font";
$flag = 1;
($fh, $feat_all_fn) = tempfile(); $fh->close;
if ($opt_d) {print "feat_all_fn: $feat_all_fn\n"}
#$feat_all_fn = substr($font_fn, 0, -4) . "_feat_all.xml";
Table_extract($font, $feat_all_fn, 0);
$font->release;
} else #applyset_xml
{
($feat_all_fn, $feat_set_fn, $font_fn) = ($ARGV[1], $ARGV[2], $ARGV[3]);
}
Feat_All_parse($feat_all_fn, %feat_all, %feat_tag);
Feat_Set_parse($feat_set_fn, %feat_tag, $feat_set, %line_metrics);
if ($opt_d) {print "feat_set = $feat_set\n";}
if ($opt_d && defined $line_metrics{'metrics'})
{print "line_metrics = \'$line_metrics{'font'}\' $line_metrics{'em-sqr'} $line_metrics{'metrics'}\n";}
Feat_Set_cmds(%feat_all, $feat_set, @commands);
if ($opt_d) {print "commands: \n"; foreach (@commands) {print "$_->{'cmd'}: $_->{'args'}\n"}; print "\n";}
$font = Font::TTF::Font->open($font_fn) or die "Can't open font";
Cmds_exec($font, @commands, %feat_all, %line_metrics);
Font_ids_update($font, %feat_all, $feat_set, %feat_tag);
#delete feat_all and embed feat_set file in font
if (defined $font->{$table_nm})
{
delete $font->{$table_nm};
}
Table_add($font, $feat_set_fn, 0);
$font_out_fn = $opt_o ? $opt_o : substr($font_fn, 0, -4) . '_tt.ttf';
$font->out($font_out_fn);
$font->release;
if ($flag)
{unlink($feat_all_fn);}
}
elsif ($cmd eq 'add')
{ #add feat_all XML (or feat_set XML with -f option) to font
if (scalar @ARGV != 3)
{Usage_print;}
if ($opt_d) {print "adding $table_nm table to font\n";}
($feat_all_fn, $font_fn) = ($ARGV[1], $ARGV[2]);
my ($feat_all_test);
$font = Font::TTF::Font->open($font_fn) or die "Can't open font\n";
if (not defined $opt_f)
{$feat_all_test = 1;}
else
{$feat_all_test = 0;}
Table_add($font, $feat_all_fn, $feat_all_test);
$font_out_fn = $opt_o ? $opt_o : substr($font_fn, 0, -4) . '_tt.ttf';
$font->out($font_out_fn);
$font->release;
}
elsif ($cmd eq 'extract')
{ #write feat_all or feat_set XML embedded in font to an XML file
if (scalar @ARGV != 3)
{Usage_print;}
if ($opt_d) {print "extracting $table_nm table from font\n";}
my ($feat_fn, $feat_set_test);
($font_fn, $feat_fn) = ($ARGV[1], $ARGV[2]);
$font = Font::TTF::Font->open($font_fn) or die "Can't open font";
if (not defined $opt_f)
{$feat_set_test = 1;}
else
{$feat_set_test = 0;}
Table_extract($font, $feat_fn, $feat_set_test);
$font->release;
}
elsif ($cmd eq 'delete')
{ #delete feat_all or feat_set XML from a font
if (scalar @ARGV != 2)
{Usage_print;}
if ($opt_d) {print "deleting $table_nm table from font\n";}
$font_fn = $ARGV[1];
$font = Font::TTF::Font->open($font_fn) or die "Can't open font";
#delete our XML table $table_nm from the ttf
if (not defined $font->{$table_nm})
{print "no $table_nm table in font\n";}
else
{delete $font->{$table_nm};}
$font_out_fn = $opt_o ? $opt_o : substr($font_fn, 0, -4) . '_tt.ttf';
$font->out($font_out_fn);
$font->release;
}
else
{
Usage_print;
}
if ($opt_d) {print "All operations completed\n";}
}
cmd_line_exec(@ARGV);
1;