#!/usr/bin/perl use strict; use Font::TTF::Scripts::Volt; use Data::Dumper; use Pod::Usage; use Getopt::Std; # don't forget Font::TTF::GDEF our $DEBUG = 0; my %opts; my $VERSION; our $CHAIN_CALL; our ($if, $of); $VERSION = 0.01; # MJPH 11-OCT-2007 First release unless ($CHAIN_CALL) { getopts('a:d:hirt:x:', \%opts); unless (defined $ARGV[1] || defined $opts{h}) { pod2usage(1); exit; } if ($opts{h}) { pod2usage( -verbose => 2, -noperldoc => 1); exit; } $if = Font::TTF::Scripts::Volt->read_font($ARGV[0], $opts{a}) || die "Can't read font $ARGV[0]"; foreach my $t (qw(GPOS GSUB GDEF)) { delete $if->{'font'}{$t}; } } Font::TTF::Scripts::Volt::main($if, %opts); unless ($CHAIN_CALL) { $if->{'font'}->update->out($ARGV[1]) || die "Can't write to font file $ARGV[1]. Do you have it installed?" unless ($DEBUG > 1);} if ($opts{'d'} || $DEBUG) { foreach (qw(GSUB GPOS)) { delete $if->{'font'}{$_}{' PARENT'}; print Dumper($if->{'font'}{$_}); } print Dumper($if->{'voltdat'}); } die $if->{'WARNINGS'} if $if->{'cWARNINGS'}; package Font::TTF::Scripts::Volt; use Font::TTF::Coverage; use Font::TTF::GSUB; use Font::TTF::GPOS; use Font::TTF::GDEF; use IO::File; my $TYPE2; # To do: resolve when to generate FMT1 or FMT2 pair adjustments sub main { my ($fv, %opts) = @_; my ($volt_text, $font); $font = $fv->{'font'}; if ($opts{'t'}) { my ($inf) = IO::File->new("< $opts{'t'}") || die "Can't open file $opts{'t'}"; while (<$inf>) { $volt_text .= $_; } $inf->close; } elsif (defined $font->{'TSIV'}) { $volt_text = $font->{'TSIV'}->read->{' dat'}; } else { die "No VOLT table in the font, nothing to do"; } delete $font->{'TSIV'} unless $opts{'r'}; # remove the volt source if ($opts{'d'}) { $::RD_HINT = 1; $::RD_TRACE = $opts{'d'} if ($opts{'d'} > 1); } $fv->{'voltdat'} = $fv->parse_volt($volt_text); $font->{'GSUB'} = Font::TTF::GSUB->new(PARENT => $font, read => 1) unless (defined $font->{'GSUB'}); $font->{'GSUB'}{'Version'} = 1; $font->{'GPOS'} = Font::TTF::GPOS->new(PARENT => $font, read => 1) unless (defined $font->{'GPOS'}); $font->{'GPOS'}{'Version'} = 1; $fv->features_ttf(%opts); foreach my $t (qw(GSUB GPOS)) { next unless (defined $font->{$t}{'FEATURES'}); $font->{$t}{'FEATURES'}{'FEAT_TAGS'} = [sort {$fv->{'featmap'}{$a} cmp $fv->{'featmap'}{$b}} @{$font->{$t}{'FEATURES'}{'FEAT_TAGS'}}]; $fv->lookups_ttf($t); $font->{$t}->dirty; } $fv->add_gdef(); # $font->{'OS/2'}->read->{'maxLookups'} = $fv->{'maxcontext'}; # print STDERR "maxcontext=$fv->{'maxcontext'}\n"; return unless ($opts{'x'}); my ($xfh) = IO::File->new("> $opts{'x'}") || die "Can't open $opts{'x'} for writing"; my (%feattags, %multis); $xfh->print("\n"); $xfh->print("\n"); foreach my $t(qw(GSUB GPOS)) { foreach my $f (@{$font->{$t}{'FEATURES'}{'FEAT_TAGS'}}) { my ($val) = $fv->{'featmap'}{$f}; if (defined $feattags{$val}) { if ($feattags{$val} eq "") { $feattags{$val} = "0"; } else { $feattags{$val}++; } $val = "$val _$feattags{$val}"; } else { $feattags{$val} = ""; } $xfh->print(" \n"); } } foreach my $l (@{$fv->{'voltdat'}{'lookups'}}) { my ($id) = $l->{'id'}; next unless ($id); my ($multi); if ($id =~ s/\\.*$//o) { if ($multis{$id}) { next; } else { $multis{$id} = 1; } } $xfh->print(" \n"); } $xfh->print("\n"); $xfh->close; } sub features_ttf { my ($self, %opts) = @_; my ($dat) = $self->{'voltdat'}; my ($font) = $self->{'font'}; my (%multis); my ($t, $s, $l, $ft, $k); # Script tag, script struct, lang struct, feature tag, lookup name. # Lookups don't actually have tags (they are just numbered). Volt users give lookups names, however, # and these names link features to lookups, so we have to be able to find lookups by name. # So first time through this we create a hash to map lookup names to lookup: $dat->{'lookuptags'} = {map {$_->{'id'} => $_} @{$dat->{'lookups'}}} unless (defined $dat->{'lookuptags'}); if ($opts{'i'}) { foreach (@{$dat->{'lookups'}}) { $_->{' include'} = 1; } } else { # There can be unused lookups in the VOLT source, i.e., lookups which aren't tied to a feature. # Mark the needed lookups by setting ' include' on the lookup. foreach $t (sort keys %{$dat->{'scripts'}}) # For each script name in VOLT source (in alpha order) { my ($s) = $dat->{'scripts'}{$t}; foreach $l (@{$s->{'langs'}}) # For each VOLT lang within this script { foreach $ft (sort keys %{$l->{'features'}}) # For each VOLT feature tag within this lang { foreach $k (@{$l->{'features'}{$ft}{'lookups'}}) # For each VOLT lookup name within this feature { $dat->{'lookuptags'}{$k}{' include'} = 1; } } } } } # Calculate lookup indicies, assign to ' index'. # GSUB and GPOS lookups are numbered independently, starting at 0. # VOLT lookups can be grouped, e.g. lookup\0, lookup\1, lookup\2, to create sub-tables of a single OT lookup. # If a lookup group is detected, set the ' sub' field on all but first of group. foreach $l (@{$dat->{'lookups'}}) { next unless ($l->{' include'}); my ($multi); if ($l->{'id'} =~ m/^([^\\]+)\\/o) { $multi = $1; } if (defined $multi && defined $multis{$multi}) { $l->{' index'} = $multis{$multi}; $l->{' sub'} = 1; } else { if ($l->{'lookup'}[0] eq 'sub') { $l->{' index'} = $dat->{'GSUB_counters'}++; } else { $l->{' index'} = $dat->{'GPOS_counters'}++; } $multis{$multi} = $l->{' index'} if (defined $multi); } } # Initialize OT script/lang/feature structure foreach $t (sort keys %{$dat->{'scripts'}}) { my ($s) = $dat->{'scripts'}{$t}; foreach $l (@{$s->{'langs'}}) { foreach $ft (sort keys %{$l->{'features'}}) { my ($type); foreach $k (@{$l->{'features'}{$ft}{'lookups'}}) { next if ($dat->{'lookuptags'}{$k}{' sub'}); if ($dat->{'lookuptags'}{$k}{'lookup'}[0] eq 'sub') { $type = 'GSUB'; } else { $type = 'GPOS'; } $self->append_feat($font, $s->{'tag'}, $l->{'tag'}, $ft, $dat->{'lookuptags'}{$k}{' index'}, $type); } } } } } sub append_feat { my ($self, $font, $script, $lang, $feat, $lindex, $type) = @_; my ($fname) = "${feat}_${script}_${lang}"; my ($othertype) = ($type eq 'GSUB') ? 'GPOS' : 'GSUB'; $self->{'featmap'}{$fname} = $feat; if ($lang eq 'dflt') { $font->{$type}{'SCRIPTS'}{$script}{'DEFAULT'} = {' REFTAG' => 'dflt'}; $font->{$othertype}{'SCRIPTS'}{$script}{'DEFAULT'} = {' REFTAG' => 'dflt'}; } unless (defined $font->{$type}{'SCRIPTS'}{$script}{$lang}) { push (@{$font->{$type}{'SCRIPTS'}{$script}{'LANG_TAGS'}}, $lang); push (@{$font->{$othertype}{'SCRIPTS'}{$script}{'LANG_TAGS'}}, $lang); $font->{$othertype}{'SCRIPTS'}{$script}{$lang}{'I Exist'} = 1; } unless (defined $font->{$type}{'FEATURES'}{$fname}) { push (@{$font->{$type}{'FEATURES'}{'FEAT_TAGS'}}, $fname); } unless (grep {$_ eq $fname} @{$font->{$type}{'SCRIPTS'}{$script}{$lang}{'FEATURES'}}) { push (@{$font->{$type}{'SCRIPTS'}{$script}{$lang}{'FEATURES'}}, $fname); } push (@{$font->{$type}{'FEATURES'}{$fname}{'LOOKUPS'}}, $lindex); } sub lookups_ttf { my ($self, $type) = @_; my ($dat) = $self->{'voltdat'}; my ($font) = $self->{'font'}; my ($ltype) = lc($type); my ($clr, @clr); my ($l, $c, $lk, $g, $i, $e, $index); $ltype =~ s/^g//o; foreach $l (sort {$a->{' index'} <=> $b->{' index'}} grep {$_->{'lookup'}[0] eq $ltype && defined $_->{' index'} && $_->{' include'}} @{$dat->{'lookups'}}) { my ($flags); $flags |= 1 if ($l->{'dir'} =~ /RTL/oi); $flags |= 2 if (!$l->{'base'}); $flags |= 8 if ($l->{'marks'} =~ m/SKIP/oi); if ($l->{'all'} and $l->{'all'} ne 'ALL') { if ($l->{'marks'} =~ m/GLYPH_SET/oi) { # Use mark filter sets if (!defined $self->{'alls'}{'*'.$l->{'all'}}) # '*', as in VOLT UI, used here to differentiate marksets from mark classes (below) { push @{$self->{'marksets'}}, $self->make_coverage(@{$dat->{'groups'}{$l->{'all'}}}); $self->{'alls'}{'*'.$l->{'all'}} = $#{$self->{'marksets'}}; } $flags |= 0x10; $font->{$type}{'LOOKUP'}[$l->{' index'}]{'FILTER'} = $self->{'alls'}{'*'.$l->{'all'}}; } else { # Use mark classes if (!defined $self->{'alls'}{$l->{'all'}}) { $self->{'alls'}{$l->{'all'}} = ++$self->{'max_all'}; foreach $g (@{$self->or_context_glyphs(@{$dat->{'groups'}{$l->{'all'}}})}) { $dat->{'glyphs'}[$g]{'mark_class'} = $self->{'alls'}{$l->{'all'}}; } } $flags |= $self->{'alls'}{$l->{'all'}} << 8; } } $font->{$type}{'LOOKUP'}[$l->{' index'}]{'FLAG'} = $flags; $font->{$type}{'LOOKUP'}[$l->{' index'}]{' index'} = $l->{' index'}; if (defined $l->{'contexts'}[0][1]) { $font->{$type}{'LOOKUP'}[$l->{' index'}]{'TYPE'} = $type eq 'GSUB' ? 6 : 8; # use a context chaining subrule and then add a new lookup for the action # always use format 3, it's simpler (same as VOLT does) if ($type eq 'GSUB') { $clr = {'MATCH' => [], 'ACTION' => [[0, $dat->{"${type}_counters"}]]}; @clr = ({%$clr}); foreach $lk (@{$l->{'lookup'}[1]}) { for ($i = 0; $i < @{$lk->[0]}; $i++) { my ($glyphs) = $self->scon_glyphs($lk->[0][$i]); if (!defined $clr->{'MATCH'}[$i]) { $clr->{'MATCH'}[$i] = Font::TTF::Coverage->new(1); } foreach (@{$glyphs}) { $clr->{'MATCH'}[$i]->add($_); } $clr->{'MATCH'}[$i]->sort; } } } else { @clr = (); foreach $lk (@{$l->{'lookup'}[1]}) { my (@match); if ($lk->{'type'} eq 'ATTACH') { # Can't use make_coverage() because $lk->{'to'} is different my ($cover) = Font::TTF::Coverage->new(1); foreach my $c (@{$lk->{'to'}}) { foreach my $g (@{$self->scon_glyphs($c->[0])}) { $cover->add($g); } } $cover->sort; push (@match, $cover); } elsif ($lk->{'type'} eq 'ADJUST_PAIR') { push (@match, $self->make_coverage(@{$lk->{'first'}}), $self->make_coverage(@{$lk->{'second'}})); } elsif ($lk->{'type'} eq 'ATTACH_CURSIVE') { push (@match, $self->make_coverage(@{$lk->{'exits'}}), $self->make_coverage(@{$lk->{'enters'}})); } else # ADJUST_SINGLE { push (@match, $self->make_coverage(@{$lk->{'context'}})); } push (@clr, {MATCH => [@match], ACTION => [[0, $dat->{"${type}_counters"}]]}); } } foreach $c (@{$l->{'contexts'}}) # multiple context subrules { my ($contlength); my ($clr1) = {}; foreach $e (@{$c}[1..$#{$c}]) { push (@{$clr1->{$e->[0] eq 'LEFT' ? 'PRE' : 'POST'}}, $self->make_coverage(@{$e}[1..$#{$e}])); } if (exists $clr1->{'PRE'} && @{$clr1->{'PRE'}}) { $clr1->{'PRE'} = [reverse (@{$clr1->{'PRE'}})]; $contlength = scalar @{$clr1->{'PRE'}}; } $contlength += scalar @{$clr1->{'POST'}} if exists $clr1->{'POST'}; foreach (@clr) { if ($c->[0] =~ m/^EXCEPT/o) { # first the full context match that does nothing push (@{$font->{$type}{'LOOKUP'}[$l->{' index'}]{'SUB'}}, {'FORMAT' => 3, 'MATCH_TYPE' => 'o', 'ACTION_TYPE' => 'l', 'RULES' => [[{'MATCH' => $_->{'MATCH'}, 'ACTION' => [], %$clr1}]]}); # then just match the core string and do the lookup push (@{$font->{$type}{'LOOKUP'}[$l->{' index'}]{'SUB'}}, {'FORMAT' => 3, 'MATCH_TYPE' => 'o', 'ACTION_TYPE' => 'l', 'RULES' => [[{%$_}]]}); } else { push (@{$font->{$type}{'LOOKUP'}[$l->{' index'}]{'SUB'}}, {'FORMAT' => 3, 'MATCH_TYPE' => 'o', 'ACTION_TYPE' => 'l', 'RULES' => [[{%$_, %$clr1}]]}); } } $contlength += scalar @{$clr->{'MATCH'}} if exists $clr->{'MATCH'}; $self->{'maxcontext'} = $contlength if ($contlength > $self->{'maxcontext'}); # print STDERR "cl=$contlength ($ltype $l->{' index'})\n"; } # Compute index for the chained lookup; be sure to copy flag word to chained lookup $index = $dat->{"${type}_counters"}++; $font->{$type}{'LOOKUP'}[$index]{'FLAG'} = $flags; } else { $index = $l->{' index'}; } if ($type eq 'GSUB') { $self->add_gsub_lookup($index, $l); } else { $self->add_gpos_lookup($index, $l); } } } sub add_gsub_lookup { my ($self, $index, $l) = @_; my ($dat) = $self->{'voltdat'}; my ($font) = $self->{'font'}; my ($s, $maxi, $maxo, @map, $sub, $i, $j); # which of the 3 types of subst are we? 1:1, 1:many, many:1 foreach $s (@{$l->{'lookup'}[1]}) { my ($sm) = scalar @{$s->[1]}; $maxo = $sm if ($sm > $maxo); my ($m) = scalar @{$s->[0]}; $maxi = $m if ($m > $maxi); } $self->{'maxcontext'} = $maxi if ($maxi > $self->{'maxcontext'}); # print STDERR "cl=$maxi (gsub $index)\n"; if ($maxi == 1 && $maxo == 1) { $sub = {'ACTION_TYPE' => 'g', 'FORMAT' => 2, ' index' => $index}; $font->{'GSUB'}{'LOOKUP'}[$index]{'TYPE'} = 1; $sub->{'COVERAGE'} = Font::TTF::Coverage->new(1); foreach $s (@{$l->{'lookup'}[1]}) { my (@input) = @{$self->scon_glyphs($s->[0][0])}; my (@output) = @{$self->scon_glyphs($s->[1][0])}; for ($i = 0; $i < @input; $i++) { my ($j) = $sub->{'COVERAGE'}->add($input[$i]); push (@{$sub->{'RULES'}[$j][0]{'ACTION'}}, $output[$i]); if ($sub->{'ACTION_TYPE'} eq 'g' && scalar @{$sub->{'RULES'}[$j][0]{'ACTION'}} > 1) { $font->{'GSUB'}{'LOOKUP'}[$index]{'TYPE'} = 3; $sub->{'ACTION_TYPE'} = 'a'; $sub->{'FORMAT'} = 1; } } } } elsif ($maxi == 1) { $sub = {'ACTION_TYPE' => 'g', 'FORMAT' => 1, ' index' => $index}; my ($i); $font->{'GSUB'}{'LOOKUP'}[$index]{'TYPE'} = 2; $sub->{'COVERAGE'} = Font::TTF::Coverage->new(1); foreach $s (@{$l->{'lookup'}[1]}) { my ($input) = $self->scon_glyphs($s->[0][0]); my (@output) = $self->context_glyphs(@{$s->[1]}); for ($i = 0; $i < @{$input}; $i++) { $sub->{'RULES'}[$sub->{'COVERAGE'}->add($input->[$i])][0]{'ACTION'} = $output[$i]; } } } else # ligature - many:1 { $sub = {'ACTION_TYPE' => 'g', 'MATCH_TYPE' => 'g', 'FORMAT' => 1, ' index' => $index}; my (@input, @new_input); $font->{'GSUB'}{'LOOKUP'}[$index]{'TYPE'} = 4; $sub->{'COVERAGE'} = Font::TTF::Coverage->new(1); foreach $s (@{$l->{'lookup'}[1]}) { my ($first) = $self->scon_glyphs($s->[0][0]); my (@all) = $self->context_glyphs(@{$s->[0]}); my (@output) = $self->context_glyphs(@{$s->[1]}); for ($i = 0; $i < @{$first}; $i++) { my (@i) = grep {$all[$_][0] == $first->[$i]}(0 .. $#all); foreach $j (@i) { push (@{$sub->{'RULES'}[$sub->{'COVERAGE'}->add($first->[$i])]}, {'ACTION' => $output[$j], 'MATCH' => [@{$all[$j]}[1..$#{$all[$j]}]]}); } } } } @map = $sub->{'COVERAGE'}->sort; $sub->{'RULES'} = [map {$sub->{'RULES'}[$map[$_]]} (0 .. @map-1)]; push (@{$font->{'GSUB'}{'LOOKUP'}[$index]{'SUB'}}, $sub); return; } sub add_gpos_lookup { my ($self, $index, $l) = @_; my ($dat) = $self->{'voltdat'}; my ($font) = $self->{'font'}; my ($s, $i, $g, $r, $c, $k, $contlength, @map); foreach $s (@{$l->{'lookup'}[1]}) { if ($s->{'type'} eq 'ADJUST_SINGLE') { my ($cover) = Font::TTF::Coverage->new(1); my (@rules); $font->{'GPOS'}{'LOOKUP'}[$index]{'TYPE'} = 1; my ($sub) = { 'FORMAT' => 2, 'ACTION_TYPE' => 'v', 'COVERAGE' => $cover}; for ($i = 0; $i < @{$s->{'context'}}; $i++) { foreach $g (@{$self->scon_glyphs($s->{'context'}[$i])}) { next unless ($cover->add($g) > $#rules); push (@rules, [{'ACTION' => [{make_value(%{$s->{'adj'}[$i]})}]}]); } } @map = $sub->{'COVERAGE'}->sort; $sub->{'RULES'} = [map {$rules[$map[$_]]} (0 .. @map-1)]; push (@{$font->{'GPOS'}{'LOOKUP'}[$index]{'SUB'}}, $sub); $contlength = 1; } elsif ($TYPE2 && $s->{'type'} eq 'ADJUST_PAIR') { my ($cover1) = Font::TTF::Coverage->new(0); my ($cover2) = Font::TTF::Coverage->new(0); my ($cover) = Font::TTF::Coverage->new(1); my (@rules, @firsts, @seconds); $font->{'GPOS'}{'LOOKUP'}[$index]{'TYPE'} = 2; my ($sub) = { 'FORMAT' => 2, 'ACTION_TYPE' => 'p', 'MATCH_TYPE' => 'g', 'COVERAGE' => $cover, 'CLASS' => $cover1, 'MATCH' => [$cover2]}; my ($c, $g, $count); for ($i = 0; $i < @{$s->{'first'}}; $i++) { foreach $g (@{$self->scon_glyphs($s->{'first'}[$i])}) { $cover1->add($g, $i+1); } } foreach $g (sort {$a <=> $b} keys %{$cover1->{'val'}}) { $cover->add($g); } for ($i = 0; $i < @{$s->{'second'}}; $i++) { foreach $g (@{$self->scon_glyphs($s->{'second'}[$i])}) { $cover2->add($g, $i+1); } } foreach $r (@{$s->{'adj'}}) { $sub->{'RULES'}[$r->[0]][$r->[1]]{'ACTION'} = [{make_value(%{$r->[2][0]})}, {make_value(%{$r->[2][1]})}]; } push (@{$font->{'GPOS'}{'LOOKUP'}[$index]{'SUB'}}, $sub); $contlength = 2; } elsif ($s->{'type'} eq 'ADJUST_PAIR') { my (@rules); my ($cover) = Font::TTF::Coverage->new(1); $font->{'GPOS'}{'LOOKUP'}[$index]{'TYPE'} = 2; $font->{'GPOS'}{'LOOKUP'}[$index]{'SUB'} = [{ 'FORMAT' => 1, 'ACTION_TYPE' => 'p', 'MATCH_TYPE' => 'g', 'COVERAGE' => $cover}]; foreach $r (@{$s->{'adj'}}) { my (@seconds) = @{$self->scon_glyphs($s->{'second'}[$r->[1]-1])}; my ($action) = [{make_value(%{$r->[2][0]})}, {make_value(%{$r->[2][1]})}]; foreach (@seconds) { push (@{$rules[$r->[0]-1]}, {'MATCH' => [$_], 'ACTION' => $action}); } } for ($i = 0; $i < @{$s->{'first'}}; $i++) { next unless ($rules[$i]); $rules[$i] = [sort {$a->{'MATCH'}[0] <=> $b->{'MATCH'}[0]} @{$rules[$i]}]; foreach $g (@{$self->scon_glyphs($s->{'first'}[$i])}) { $font->{'GPOS'}{'LOOKUP'}[$index]{'SUB'}[0]{'RULES'}[$cover->add($g)] = $rules[$i]; } } $contlength = 2; } elsif ($s->{'type'} eq 'ATTACH_CURSIVE') { my ($cover) = Font::TTF::Coverage->new(1); $font->{'GPOS'}{'LOOKUP'}[$index]{'TYPE'} = 3; my ($sub) = { 'FORMAT' => 1, 'ACTION_TYPE' => 'e', 'COVERAGE' => $cover}; foreach $g (@{$self->or_context_glyphs(@{$s->{'enters'}})}) { $sub->{'RULES'}[$cover->add($g)][0]{'ACTION'}[0] = make_anchor($dat->{'glyphs'}[$g]{'anchors'}{'entry'}[0]); } foreach $g (@{$self->or_context_glyphs(@{$s->{'exits'}})}) { $sub->{'RULES'}[$cover->add($g)][0]{'ACTION'}[1] = make_anchor($dat->{'glyphs'}[$g]{'anchors'}{'exit'}[0]); } push (@{$font->{'GPOS'}{'LOOKUP'}[$index]{'SUB'}}, $sub); $contlength = 2; } elsif ($s->{'type'} eq 'ATTACH') { my ($basec) = Font::TTF::Coverage->new(1); my ($markc) = Font::TTF::Coverage->new(1); my ($acount, %anchors, @marks, @rules, $type); foreach $c (@{$s->{'to'}}) { $anchors{$c->[1]} = $acount++ unless (defined $anchors{$c->[1]}); foreach $g (@{$self->scon_glyphs($c->[0])}) { $marks[$markc->add($g)] = [$anchors{$c->[1]}, make_anchor($dat->{'glyphs'}[$g]{'anchors'}{"MARK_$c->[1]"}[0])] if ($g); } } foreach $g (sort {$a <=> $b} @{$self->or_context_glyphs(@{$s->{'context'}})}) { next unless ($g); my ($i) = $basec->add($g); my $thistype = ($dat->{'glyphs'}[$g]{'type'} eq 'MARK') ? 6 : # mark-to-mark ($dat->{'glyphs'}[$g]{'type'} eq 'LIGATURE') ? 5 : # mark-to-ligature 4; # mark-to-base if ($type) { $self->error("Mix of base character types in lookup $l->{'id'} at glyph $dat->{'glyphs'}[$g]{'name'}\n") if $type != $thistype; } else { $type = $thistype; } foreach $k (sort {$anchors{$a} <=> $anchors{$b}} keys %anchors) { if (defined $dat->{'glyphs'}[$g]{'anchors'}{$k}) { for my $comp (0 .. $#{$dat->{'glyphs'}[$g]{'anchors'}{$k}}) { if (defined $dat->{'glyphs'}[$g]{'anchors'}{$k}[$comp]) { $rules[$i][$comp]{'ACTION'}[$anchors{$k}] = make_anchor($dat->{'glyphs'}[$g]{'anchors'}{$k}[$comp]); } } } } } $font->{'GPOS'}{'LOOKUP'}[$index]{'TYPE'} = $type; push (@{$font->{'GPOS'}{'LOOKUP'}[$index]{'SUB'}}, { 'FORMAT' => 1, 'COVERAGE' => $basec, 'MATCH' => [$markc], 'MARKS' => [@marks], 'RULES' => [@rules]}); $contlength = 2; } } $self->{'maxcontext'} = $contlength if ($contlength > $self->{'maxcontext'}); # print STDERR "cl=$contlength (gpos $index\n"; } sub add_gdef { my ($self) = @_; my ($font) = $self->{'font'}; my ($dat) = $self->{'voltdat'}; my ($gdc) = Font::TTF::Coverage->new(0); # glyph types my ($gdm) = Font::TTF::Coverage->new(0); my %types = ( 'BASE' => 1, 'LIGATURE' => 2, 'MARK' => 3, 'COMPONENT' => 4 ); my ($g); foreach $g (@{$dat->{'glyphs'}}) { $gdc->add($g->{'gnum'}, $types{$g->{'type'}}); $gdm->add($g->{'gnum'}, $g->{'mark_class'}) if (defined $g->{'mark_class'}); } $font->{'GDEF'} = Font::TTF::GDEF->new('parent' => $font, 'read' => 1); # $font->{'GDEF'}{'Version'} = 1.0; # Let GDEF.pm figure out version $font->{'GDEF'}{'GLYPH'} = $gdc; $font->{'GDEF'}{'MARKS'} = $gdm if ($gdm->{'max'} > 0); $font->{'GDEF'}{'MARKSETS'} = [ @{$self->{'marksets'}} ] if exists $self->{'marksets'}; $font->{'GDEF'}->dirty; } =begin comment Takes a lookup context list (as from IN_CONTEXT) and returns an array of flattened arrays of glyph ids that map to the context list. =end comment =cut sub context_glyphs { my ($dat, @list) = @_; my (@input, @new_input, $l, $g); foreach $l (@list) { my ($glyphs) = $dat->scon_glyphs($l); my (@new_input) = (); foreach $g (@$glyphs) { push (@new_input, @input ? (map {[@$_, $g]} @input) : [$g]); } @input = @new_input; } return @input; } =begin comment Takes a context list and returns a flattened array of glyph ids, duplicates removed and sorted in order of appearance in the context list. (If a glyph appears multiple times, the earliest one is kept). [This is very un-VOLTish since order within a group does not matter. Need to investigate. -bh] =end comment =cut sub or_context_glyphs { my ($dat, @list) = @_; my ($l, $g, %res, $c); foreach $l (@list) { my ($glyphs) = $dat->scon_glyphs($l); foreach $g (@{$glyphs}) { $res{$g} = ++$c unless (defined $res{$g}); } } return [sort {$res{$a} <=> $res{$b}} keys %res]; } =begin comment Takes single C and returns a array of glyph ids. The array is normally sorted by glyph id, but not so in the case of an ENUM -- this might be a bug. =end comment =cut sub scon_glyphs { my ($dat, $context) = @_; if ($context->[0] eq 'GLYPH') { return defined $context->[1] ? [$context->[1]] : warn("Undefined glyph"); } elsif ($context->[0] eq 'GROUP') { return defined $dat->{'voltdat'}{'groups'}{$context->[1]} ? [sort {$a <=> $b} @{$dat->or_context_glyphs(@{$dat->{'voltdat'}{'groups'}{$context->[1]}})}] : warn "Unknown glyph group $context->[1]"; } elsif ($context->[0] eq 'RANGE') { return [$context->[1] .. $context->[2]]; } elsif ($context->[0] eq 'ENUM') { return $dat->or_context_glyphs($context->[1]); } return []; } sub make_coverage { my ($self, @contexts) = @_; my ($cover) = Font::TTF::Coverage->new(1); my ($c); foreach $c (@contexts) { my ($glyphs) = $self->scon_glyphs($c); foreach (@{$glyphs}) { $cover->add($_); } } $cover->sort; return $cover; } sub make_value { my (%pos) = @_; my (%res, $s); my %map = ( 'x' => ['XPlacement', 'XPlaDevice'], 'y' => ['YPlacement', 'YPlaDevice'], 'adv' => ['XAdvance', 'XAdvDevice']); foreach $s (qw(x y adv)) { if (defined $pos{$s}) { $res{$map{$s}[0]} = $pos{$s}[0]; if (defined $pos{$s}[1]) { # $res{$map{$s}[1]} = make_delta($pos{$s}[1]); } } } %res } sub make_anchor { my ($point) = @_; my (%res, $s, $r); return undef unless (defined $point->{'pos'}); return ($point->{'anchor'}) if defined ($point->{'anchor'}); foreach $s (qw(x y)) { $res{$s} = $point->{'pos'}{$s}[0]; } $r = Font::TTF::Anchor->new(%res); $point->{'anchor'} = $r; return $r; } __END__ =head1 TITLE volt2ttf - compiles volt code into OT tables in a font =head1 SYNOPSIS volt2ttf [-a attach.xml] [-t volt.txt] infile.ttf outfile.ttf Compiles volt source into OT tables in the font. =head1 OPTIONS -a file Attachment point database [only needed for anchor attachment] -i Include all lookups even those not referenced by a used feature -r Do not delete Volt source table in the font -t file Volt source as text file to use instead of what is in the font -h Help -x file Generate TypeTuner aliases .xml file =head1 DESCRIPTION volt2ttf is like loading a font into VOLT and hitting compile and saving the result. Note that it doesn't compile a new cmap, though. Just the OT tables are built. =cut