The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
#!/usr/bin/perl
#{([
use strict;
use File::Basename;
use Getopt::Std;
use Data::Dumper;
   
use Font::TTF::Font;
use Font::TTF::OTTags qw( %ttnames readtagsfile);

our ($opt_a, $opt_d, $opt_g, $opt_l, $opt_r, $opt_s, $opt_t, $opt_v, $VERSION);

$VERSION = '0.53';

my $progname = basename ($0);
$progname =~ s/\.[^.]+$//;

getopts('ad:g:l:rst:v:');

die <<"EOT" unless $#ARGV >= 0 and $#ARGV <= 1;;

$progname -- create VOLT project from existing OpenType font file

syntax: $progname [options] infontfile [outfontfile]

Attempts to create a VOLT project from an existing OpenType font by reading and
interpreting the existing GDEF, GPOS, and GSUB tables. Not every OpenType
rule can be mimiced in VOLT; warnings are issued when $progname cannot
handle something.

In normal usage, specify either outfontfile (to create a font
ready to be opened by VOLT) and/or the -v option (to create a
VOLT project source that can be imported into the font).

options:
    -a        allow non-adobe glyph names
    -g n      group creation threshold
    -l file   emit log messages to named file
    -r        retain GPOS, GSUB, and GDEF tables in output font
    -s        do not send warnings to stdout (will still go to log)
    -t file   name of replacement VOLT tags file if needed.
    -v file   Volt project source (.vtp) file to create
    
The group creation threshold option sets the minimum number of glyphs
that $progname will put into a group for the purposes of building
a lookup.

Lookups currently supported  (type[.format]):

GSUB:   1 2 3 4   6.3
GPOS    1 2 3 4 5     8.3

Version $VERSION
EOT


my $font;							# Font structure
my ($cmap, $post, $gsub, $gpos, $gdef);	# Various tables in font

my $g;		# Glyph structure

# MAJOR TO-DO: Someday this should all be rewritten to generate the internal datastructure used by Volt.pm.  Then this
# would become a library module and command-line option for make_volt.  Some day.

# Glyph data are stored in structures (anonymous hashes) containing the following elements (all optional):
#	ID			glyph ID
#	NAME		glyph name, derived from postname
#	@UNICODES	an array of Unicode values
#	COMPONENTS	a count of ligature components
#	TYPE		one of BASE, MARK, LIGATURE, COMPONENT (from GDEF if present)
#   ANCHORS     a hash of attachment points, indexed by attachment point name, value is a pointer to an Font::TTF::Anchor 

# Gyph structures can be located from glyph ID, PS name or, for encoded glyphs, USV:
my (%GlyphFromID, %GlyphFromName, %GlyphFromCmapUnicode);

my ($gid, $gname, $u);	# Glyph ID, Glyph name, and Unicode

my ($SLFText, $LookupText);		# generated VOLT source texts: Script/Lang/Feature, Lookups

my ($warningCount, $genericCount);

# Open log file
open(LOG, ">$opt_l") if $opt_l;

# Sub to print warning messages to console and log. 1st parm is the message.
# 2nd param, if supplied, is a line number.

sub MyWarn {
	my ($msg, $line) = @_;
	$warningCount++;
	if (defined $line) {
		print LOG "line $line: " . $msg;
		warn "line $line: " . $msg unless $opt_s;
	} else {
		print LOG $msg;
		warn $msg unless $opt_s;
	}
}



##########################
# Open the font and verify presence of needed tables

$font = Font::TTF::Font->open($ARGV[0]) or die "Could not open font '$ARGV[0]'\n";

exists $font->{'post'} or die "Could not locate Postscript name info in font '$ARGV[0]\n" ;
$post = $font->{'post'}->read;
$cmap = $font->{'cmap'}->find_ms or die("Unable to locate Windows cmap in font '$ARGV[0]'\n");

$gdef = $font->{'GDEF'}->read if exists $font->{'GDEF'};
$gsub = $font->{'GSUB'}->read if exists $font->{'GSUB'};
$gpos = $font->{'GPOS'}->read if exists $font->{'GPOS'};

die "None of GDEF, GSUB, GPOS tables found in font '$ARGV[0]' \n  -- you probably don't need to use $progname\n" unless $gdef || $gsub || $gpos;

# $font->out_xml($opt_d, 'GSUB') if defined $opt_d;
if ($opt_d)
{
    open (DUMP, ">$opt_d")  or die "Couldn't open '$opt_d' for writing.";
    my $res;
    if (0)
    {
        # I wish this could work!  But sometimes Perl abends
        my $d = Data::Dumper->new([$gsub, $gpos, $gdef], [ qw(gsub gpos gdef)]);
        sub myfilter
        {
            my ($hash) = @_;
            my @a = grep {$_ ne ' PARENT'} (keys %{$hash}) ;
            return [ @a ];
        }
        $d->Sortkeys(\&myfilter);
        $d->Indent(3);  # I want array indicies
        $res = $d->Dump;
        $d->DESTROY;
    }
    else
    {
        $Data::Dumper::Indent = 3;
        $res = Dumper($font);
    }
    $res =~ s/  / /g;
    print DUMP $res;
    close DUMP;
    
}



##########################
# Build Glyph structure based on postscript names and cmap values.

# Note: This algorithm has to match that in VoltFixup.pl if the source
# generated by this program is going to be spliced into a modified font.
# In fact, this code is adapted from VoltFixup.pl!


# loop through all glyphs, setting up glyph structure.


GLYPH: for $gid (0 .. $font->{'maxp'}{'numGlyphs'}-1) 
{
	$gname = $post->{'VAL'}[$gid];

	# $gname = "glyph$gid" unless defined $gname;
	
	($gname eq '.notdef')			&& do {
		# no PS name
		next GLYPH;
		};
	
	($gname !~ /^[a-zA-Z][a-zA-Z0-9_.]*$/)	&&  do {
		MyWarn "Glyph $gid has non-standard psname '$gname'\n";
		next GLYPH unless $opt_a;
		};

    # Only letters, digits, '.' and '_' are allowed in VOLT names
    $gname =~ s/[^\w.]/_/og;

	(exists $GlyphFromName{$gname})		&& do {
		MyWarn "Glyph name '$gname' is used more than once in font  (e.g., glyphs $GlyphFromName{$gname}{'ID'} and $gid) -- second ignored\n";
		next GLYPH;
		};

	# Ah, here is a name worth keeping!
	$g = $GlyphFromID{$gid} = $GlyphFromName{$gname} = { ID => $gid, NAME => $gname};

	if (defined $gdef) 
	{
		# Look up type in GDEF table
		my $type = $gdef->{'GLYPH'}{'val'}{$gid};
		$g->{'TYPE'} = (qw(BASE LIGATURE MARK COMPONENT))[$type-1] if $type > 0;
	}
}

#Initialize groups from gdef marks classes...
InitGroups();


# Some things not yet handled from GDEF:
if (defined $gdef)
{
    foreach (qw (ATTACH LIG))
    {
        MyWarn "GDEF $_ information not implemented\n" if exists $gdef->{$_} && $gdef->{$_}{'COVERAGE'}{'count'} > 0;
    }
}


# loop through the MS cmap and adding in Unicode info to %Glyph

CMAP: while (($u, $gid) = each %{$cmap->{'val'}}) 
{

	if (exists $GlyphFromCmapUnicode{$u}) {
		MyWarn sprintf ("Corrupt cmap: Unicode value U+%04X occurs more than once\n", $u);
		next CMAP;
	}

	if (exists $GlyphFromID{$gid}) {
		# Glyph with this id already present:
		$g = $GlyphFromID{$gid};
	} else {
		# Glyph with this id not yet present (must not have had a usable PS name), so create it:
		$g = $GlyphFromID{$gid} = { ID => $gid};
	}

	$g->{'@UNICODES'} = [] if not exists $g->{'@UNICODES'};	# Create array to hold Unicode values
	push @{$g->{'@UNICODES'}}, $u;		# Add to array of Unicode values
	$GlyphFromCmapUnicode{$u} = $g;		# Be able to find glyph via cmap from Unicode
}


# Try to locate and read VOLT tags.txt file to find names for Scripts, Languages, and Features:
if (defined $opt_t)
{
    MyWarn "Cannot open VOLT tags file '$opt_t'\n" unless readtagsfile($opt_t);
}


##########################
#   InitGroups()
#	CacheGroup()
#	GetGroup()
#	GetGroupNames()
#
# Routines to create and manage glyph groups from lists of GIDs.
# Groups are numbered from 0. Groups 0 through $maxMarkClass are actually the
# mark classes defined in GDEF and, therefore, are named "Class0", "Class1", etc.
# Beyond that, groups are created as needed for any specific list of GIDs. 
# Such groups are named, simply, "Group3", "Group4", etc.
#
# InitGroups()
#   Set first n groups based on GDEF MARKs class definition
#
# CacheGroup()
#	Accept a list of GIDs 
#	Return the name of a group.
#	If the group already exists, return its name. Else create the group
#
# GetGroup()
#	Accept a group name
#	Return a list of GIDs
#
# GetGroupNames()
#	returns a list of the names of defined groups.
#
# Current implementation:
# Groups are maintained in an array. The name of the group is implied by its index, e.g. "Group001"
# Each array element is simply the GID's join'd together by ":"
#

my @Groups;		    # Array of groups. See CacheGroup()
my $maxMarkClass;    # Highest Groups index that represents a MARK class

sub InitGroups()
{
    # initialize groups list from GDEF class...

    return unless defined $gdef && exists $gdef->{'MARKS'};
    die "Unexpected call to InitGroups\n" if $#Groups != -1 || $maxMarkClass > 0;

    my @classlist = ClassToLists ($gdef->{'MARKS'});
    $maxMarkClass = $#classlist;
    for my $class (0 .. $maxMarkClass)
    {
        $Groups[$class] = join (":", @{$classlist[$class]}) if defined $classlist[$class];
    }
}


sub CacheGroup(@)
{
	my ($i, $tmp);
	$tmp = join (":", sort {$a <=> $b} @_);
	for $i (0 .. $#Groups)
	{
		return GetGroupName($i) if $tmp eq $Groups[$i];
	}
	# Not in the cache, so add it:
	push @Groups, $tmp;
	return GetGroupName($#Groups);
}

sub GetGroup($)
{
	my $i = shift;
	$i =~ s/^\D*//;	# strip leading non-digits
	die "Invalid Group index $i." if $i < 0 or $i > $#Groups;
	return (split(":", $Groups[$i]));
}

sub GetGroupNames()
{
	return (map {GetGroupName($_) } (0 .. $#Groups));
}

sub GetGroupName($)
{
    my $i = shift;
	die "Invalid Group index $i." if $i < 0 or $i > $#Groups;
    return $i <= $maxMarkClass ? "Class$i" : "Group$i";
}

##########################
#	GetGroupRange()
#
# This is one of the more complicated subs. It is used to partition the rules for a given lookup
# into collections of rules that can be written with a group notation. 
#
# Note: This routine is written using the notation of "lhs" and "rhs" as if the lookup were GPOS type 2. That
# is the lhs of each rule is a single glyph, the rhs is a sequence of glyphs. However, the routine is
# equally usable for type 4 lookups by reversing the sense of the parameters. From a notational standpoint
# it is easier just to think of lhs & rhs
#
# The input can be thought of as column of single GIDs, each element [or row] representing the lhs of a type 2
# lookup (or the rhs of a type 4 lookup), and a 2-D matrix of GIDs each row representing the rhs 
# of type 2 lookup (or the lhs of a type 4 lookup). The first of these ($lhs) is passed as a 
# reference to an array,the second ($rhs) as a reference to an array of references to arrays (an "LOL")
#
# The only other parameter is a starting row index ($start). The routine looks at the consecutive rows
# in the input data to determine how many can be collected together into one rule. One of the return
# parameters is the ending row of the collection, which can then be incremented and passed back in
# as the start value to look for another group.
#
# More specifically, the return value is a list containing:
#
#	$last	index of the last row of the data is part of the collection; may be same as $start
#	$col	column index in $rhs indicating which of the items in the sequences of the group varies
#				(will be -1, if $end == $start)
#	$lhsGroup	name of a group representing the lhs glyphs, e.g., @{$lhs}[$start .. $last]; may be undef
#	$rhsGroup	name of a group representing the glyphs in $rhs column, i.e., @{$rhs}[$start .. $last][$col]; may be undef

sub GetGroupRange(\@\@$)	# @lhs, @rhs, $start
{
	my ($lhs, $rhs, $start) = @_;
	my ($last, $col, $lhsGroup, $rhsGroup);		# Return values
	my ($lastcol, $i);

	$lastcol = $#{$rhs->[$start]};	# last column index in sequence at row $start

ROWLOOP:
	for ($last = $start + 1; $last <= $#{$lhs}; $last++)		# For loop used so $last retains value on exit of loop
	{
		# Verify lhs GID is strictly ascending order (must be so for a valid group)
		last if $lhs->[$last] <= $lhs->[$last-1];

		# Verify that the length of this row's rhs sequence is the same as $start's:
		last if $#{$rhs->[$last]} != $lastcol;

		# Locate the varying column (there can be only one!)
		for $i (0 .. $lastcol)
		{
			# If glyph is same as on $start row, then try next column
			next if $rhs->[$start][$i] == $rhs->[$last][$i];

			# If we already know the column, and this isn't it, then this
			# row cannot be part of the collection. Also, if the GID isn't
			# strictly ascending then this row cannot be part of the collection:
			last ROWLOOP if ((defined $col) && ($i != $col)) || ($rhs->[$last][$i] <= $rhs->[$last-1][$i]);

			# Remember the target column:
			$col = $i if not defined $col;
		}
	}

	if (!defined $col || ($last - $start + 1) < $opt_g)
	{
		# Cannot build a group:
		$last = $start;
	}
	else
	{
		# At this point, $last is one greater than the last row that can be part
		# of the group, so adjust it:
		$last -=1;
	}

    
	if ($last > $start)
	{

		# Construct groups
		$lhsGroup = CacheGroup (@{$lhs}[$start .. $last]);
		$rhsGroup = CacheGroup (map { $rhs->[$_][$col]} ($start .. $last));
	}
	else
	{
		# Make sure $col won't match any column:
		$col = -1;
	}
	
	# Done!
	return ($last, $col, $lhsGroup, $rhsGroup);
}

##########################
# ClassToLists()
#
# Given a Class table, return an array (indexed by class value) of arrays of GIDS (in ascending order)

sub ClassToLists($)		# reference to Font::TTF coverage table structure
{
	my $c = shift;

	# Make sure this is a class table and not a cover definition:
	die "ClassToList() not given a class table." if $c->{'cover'} == 1;
	
	my @res;
	
	foreach my $gid (sort {$a <=> $b} keys %{$c->{'val'}})
	{
	    my $classvalue = $c->{'val'}{$gid};
	    $res[$classvalue] = [] unless defined $res[$classvalue];
	    push @{$res[$classvalue]}, $gid;
	}

	return @res;
}


##########################
# CoverToList()
#
# Given a Coverage table, return an array of GIDS in correct order

sub CoverToList($)		# reference to Font::TTF coverage table structure
{
	my $c = shift;

	# Make sure this is a coverage table and not a class definition:
	die "CoverToList() not given a coverage table." if $c->{'cover'} != 1;

	return (sort {$c->{'val'}{$a} <=> $c->{'val'}{$b}} keys %{$c->{'val'}});
}


##########################
# NameFromID()
#
# Routine to get glyph name from ID, 
# 
# if postscript name not defined in font, make one up but issue warning

sub NameFromID($)	# GID
{
	my $gid = shift;
	if (!exists $GlyphFromID{$gid}->{'NAME'})
	{
		my $uname;
		$uname = sprintf (" (U+%04X)", $GlyphFromID{$gid}->{'@UNICODES'}[0]) if exists $GlyphFromID{$gid}->{'@UNICODES'};
		
		MyWarn ("Glyph $gid$uname is used but doesn't have a name -- making one up.\n");
		$GlyphFromID{$gid}->{'NAME'} = "glyph$gid";
		$genericCount++;
	}
	return $GlyphFromID{$gid}->{'NAME'};
}

##########################
# GlyphOrGroup
#
# Many lookups allow either GROUP or GLYPH depending on whether one or many glyphs are included.
# This function takes a list of GIDS and creates the appropriate GROUP or GLYPH needed.
#

sub GlyphOrGroup
{
    if (scalar(@_) > 1)
    {
        return "GROUP \"" . CacheGroup(@_) . "\"";
    }
    else
    {
        return "GLYPH \"" . NameFromID($_[0]) . "\"";
    }
}



##########################
# WriteLookupHeader()
#
# Routine to generic lookup header to $LookupText
#

sub WriteLookupHeader ($$)		# lookup structure reference, name to be used in lookup
{
	my ($l, $name) = @_;	
	$LookupText .= "DEF_LOOKUP \"$name\"";
	# At one time I thought VOLT might be sensitive to the order these were written in, but now I don't think so.
	# In any case, this seems to be the order VOLT itself writes:

	# set SKIP_BASE only if *both* IgnoreBaseGlyphs and IgnoreLigatures bits set:
	$LookupText .= ($l->{'FLAG'} & 0x0006) == 0x0006 ? " SKIP_BASE" : " PROCESS_BASE";
	$LookupText .= ($l->{'FLAG'} & 0x0008) ? " SKIP_MARKS" : " PROCESS_MARKS " . ($l->{'FLAG'} & 0xFF00 ? GetGroupName($l->{'FLAG'}>>8) : "ALL");
	$LookupText .= " DIRECTION " . ($l->{'FLAG'} & 0x0001 ? "RTL" : "LTR");
	MyWarn (sprintf "Lookup $name has unhandled lookup flag bits 0x%04X\n", $l->{'FLAG'} & 0xF0) if $l->{'FLAG'} & 0xF0;
	$LookupText .= "\n";

}


##########################
# GetContext()
#
# Return a string representing the context 

sub GetContext($$)       # subtable and either 'PRE' or 'POST' 
{
    my ($sub, $which) = @_;
    
    my $res;
    
    foreach my $ctx ($which eq 'PRE' ? reverse (@{$sub->{'RULES'}[0][0]{$which}}) : @{$sub->{'RULES'}[0][0]{$which}} )
    {
        $res .= $which eq 'PRE' ? ' LEFT ' : ' RIGHT ';
        if ($sub->{'MATCH_TYPE'} eq 'o')
        {
            # Cover-based context -- but if just one glyph in the cover let's do it as a glyph
            $res .= GlyphOrGroup (keys %{$ctx->{'val'}}) . "\n";
        }
        else
        {
            MyWarn "MATCH_TYPE is '$sub->{'MATCH_TYPE'}' rather than expected 'o' in GetContext.\n";
        }
    }
            
    return $res;
}

##########################
# CoalesceChainContextSubtables()
#
# Chaining Context Lookups (GSUB 6, GPOS 8) need some preprocessing to coalesce adjacent subtables a into single VOLT lookup.
# While this step isn't actually needed for some situations, it is required as part of the support for "EXCEPT_CONTEXT".
# The way EXCEPT_CONTEXT works is to split the single VOLT lookup into multiple subtables where the first has zero actions but
# does have all the context, the second has no context but does have all the action. Since there is no way to express this
# in VOLT as two separate lookups (can't have zero actions), we have to coalesce these subtables.
#
# VOLT also uses separate subtables for normal "IN_CONTEXT" rules when there are multiple contexts of differing lengths.
# These wouldn't have to be coalesced into a single VOLT lookup, but since we have to handle the EXCEPT_CONTEXT we may as well
# do it all.
#
# So the following routine preprocesses the subtables of a lookup to coalesce such subtables and generate the context strings.
#

sub CoalesceChainContextSubtables ($$)
{
    my $l = shift;      # ref to chaining context lookup structure
    my $parent = shift; # ref to GSUB or GPOS table
    
    my $name = $l->{'-name'};
 
    # Loop through the subtables. 
    # Loop is constructed this way because I may be deleting subtables as I go through
    my $subtbl;
	for ($subtbl = 0; $subtbl < scalar (@{$l->{'SUB'}});   )    
	{
	    my $sub = $l->{'SUB'}[$subtbl];
	    if ($sub->{'FORMAT'} != 3) 
	        { MyWarn "Lookup '$name' has unhandled subtable format $sub->{'FORMAT'} -- ignoring subtable.\n"; splice (@{$l->{'SUB'}}, $subtbl,1); next;}
   		
   		# I don't think it is possible to have more than one RULES entry, but if a font shows
   		# up with this I want to know about it...
   		if ($#{$sub->{'RULES'}} > 0)
        	{ MyWarn "Font has more than one RULES entry for lookup '$name' (type $l->{'TYPE'} format $sub->{'FORMAT'}) -- ignoring subtable\n"; splice (@{$l->{'SUB'}}, $subtbl,1); next;}
        
        # Don't ask me why Martin has another array in the structure at this point, but he does:
        if ($#{$sub->{'RULES'}[0]} > 0)
		    { MyWarn "Font has more than one RULES[0] entry for lookup '$name' (type $l->{'TYPE'} format $sub->{'FORMAT'}) -- ignoring subtable\n"; splice (@{$l->{'SUB'}}, $subtbl,1); next;}

		# For now, cannot handle more than one ACTION item
		if ($#{$sub->{'RULES'}[0][0]{'ACTION'}} > 0)
		    { MyWarn "Font has more than one ACTION entry for lookup '$name' (type $l->{'TYPE'}  format $sub->{'FORMAT'}) -- ignoring subtable\n"; splice (@{$l->{'SUB'}}, $subtbl,1); next;}

		# In order to coalesce two subtables, the MATCH of the two subtables must be equivalent, and if both subtables have an Action, then that must match
        # So we'll calculate $matchID and $lookupID, two values that we can compare with previous and next lookups.
        # While we're at it we'll format the context strings
        
        my ($lookupID, $context, $matchID);
        
        # Start by building $lookupID from the ACTION.
        if (defined $sub->{'RULES'}[0][0]{'ACTION'}[0])
        {
            # This has a rule, so it isn't an EXCEPT_CONTEXT subtable.
       		my ($offset, $ti) = @{$sub->{'RULES'}[0][0]{'ACTION'}[0]};
    		unless (defined $ti)
    		    { MyWarn "OOPS: ACTION not defined in lookup '$name' (type $l->{'TYPE'}  format $sub->{'FORMAT'}) -- ignoring subtable\n"; splice (@{$l->{'SUB'}}, $subtbl,1); next;}

    		# For now, offset must be 0 and target rule must not be anything complex 
    		if ($offset != 0)
    		    { MyWarn "Font has nonzero ACTION offset for lookup '$name' (type $l->{'TYPE'} format $sub->{'FORMAT'}) -- ignoring subtable\n"; splice (@{$l->{'SUB'}}, $subtbl,1); next;}
    		
    		if ($parent->{'LOOKUP'}[$ti]{'TYPE'} > (ref ($parent) eq 'Font::TTF::GSUB' ? 4 : 6))
    		    { MyWarn "Font has complex lookup in ACTION entry for lookup '$name' (type $l->{'TYPE'} format $sub->{'FORMAT'}) -- ignoring subtable\n"; splice (@{$l->{'SUB'}}, $subtbl,1); next;}
    		
    		if ($#{$parent->{'LOOKUP'}[$ti]{'SUB'}} > 0)
    		    { MyWarn "Font has multi-subtable lookup in ACTION entry for lookup '$name' (type $l->{'TYPE'} format $sub->{'FORMAT'}) -- ignoring subtable\n"; splice (@{$l->{'SUB'}}, $subtbl,1); next;}
    		$lookupID = $ti;
    		$context = "IN_CONTEXT\n";
        }
        else
        {
            # This has no rule, so it is an EXCEPT_CONTEXT subtable.
            $context = "EXCEPT_CONTEXT\n";
        }
        
		# Finish constructing the context string:
		$context .= GetContext ($sub, 'PRE')  if exists $sub->{'RULES'}[0][0]{'PRE'};
        $context .= GetContext ($sub, 'POST') if exists $sub->{'RULES'}[0][0]{'POST'};
        $context .= "END_CONTEXT\n";
        
		# Now, calculate a string which uniquely describes this subtable's match sequence:
		my $matchID = '|';
		foreach my $ctx (@{$sub->{'RULES'}[0][0]{'MATCH'}})
		{
		    if ($sub->{'MATCH_TYPE'} eq 'o')
		    {
		        $matchID .= join(',', sort {$a <=> $b} keys %{$ctx->{'val'}}) . '|';
		    }
	        else
            {
                MyWarn "MATCH_TYPE is '$sub->{'MATCH_TYPE'}' rather than expected 'o' in CoalesceChainContextSubtables.\n";
            }
        }
        
        # See if we can coalesce this subtable with the previous one:
        
        if ($subtbl > 0 &&
            $l->{'SUB'}[$subtbl-1]{'-matchID'} eq $matchID &&
            (!defined $l->{'SUB'}[$subtbl-1]{'-lookupID'} || ($l->{'SUB'}[$subtbl-1]{'-lookupID'} eq $lookupID)))
        {
            # Whoo hoo! we can coalesce them:
            # First, concatinate its context with ours
            $context = $l->{'SUB'}[$subtbl-1]{'-context'} . $context;
            # Now blow it away!
            splice (@{$l->{'SUB'}}, $subtbl-1, 1) ;
        }
        else
        {
            # Couldn't coalesce, so be sure to bump loop variable for next pass:
            $subtbl++;
        }
        
        # Finally, store my private data into this subtable:
        $sub->{'-matchID'} = $matchID;
        $sub->{'-context'} = $context;
        $sub->{'-lookupID'} = $lookupID;
    }
} 



##########################
# WriteSimpleGSUBLookup()
#
# Routine to append a simple GSUB lookup (types 1 - 4) subtable to $LookupText
#

sub WriteSimpleGSUBLookup ($$$)		# lookup structure reference, name to be used in lookup, subtable index
{
	my ($l, $name, $stbl) = @_;

	my ($lhs, $rhs, $start, $last, $col, $lhsGroup, $rhsGroup);
	my ($cover, $i, $subrule);
	
	my $sub = $l->{'SUB'}[$stbl];
	
	if ($l->{'TYPE'} == 1)
	{
		# 1-1 (simple) substitution

        MyWarn ("Unexpected ACTION_TYPE '$sub->{'ACTION_TYPE'}' for lookup '$name'.\n") if $sub->{'ACTION_TYPE'} !~ /[og]/;

		if ($sub->{'FORMAT'} == 1 || $sub->{'FORMAT'} == 2)
        {
    		# Build $lhs and $rhs arrays:
    		$lhs = [ CoverToList ($l->{'SUB'}[$stbl]{'COVERAGE'}) ];
    		$rhs = ($sub->{'FORMAT'} == 1) ?
    			[ map { [ $lhs->[$_] + $sub->{'ADJUST'} ] } (0 .. $#{$lhs}) ]:		# Format 1
    			[ map { $sub->{'RULES'}[$_][0]{'ACTION'} } (0 .. $#{$lhs})  ];		# Format 2
    
    		# Write out the substitution rules:
    		$start = 0;
    		while ($start <= $#{$lhs})
    		{
    			($last, $col, $lhsGroup, $rhsGroup) = GetGroupRange(@{$lhs}, @{$rhs}, $start);
    			if ($last > $start)
    			{
    				# Write rule with groups:
    				$LookupText .= "SUB GROUP \"$lhsGroup\"\n";
    				$LookupText .= "WITH GROUP \"$rhsGroup\"\n";
    			}
    			else	
    			{
    				# Write rule with glyphs:
    				$LookupText .= "SUB GLYPH \"" . NameFromID($lhs->[$start]) . "\"\n";
    				$LookupText .= "WITH GLYPH \"" . NameFromID($rhs->[$start][0]) . "\"\n";
    			}
    			$LookupText .= "END_SUB\n";
    			$start = $last+1;
    
    		}
    	}
    	else
		{
		    MyWarn "GSUB '$name' has unrecognized format $sub->{'FORMAT'}.\n"; 
		}

	}
	elsif ($l->{'TYPE'} == 2)
	{
		# 1-n (multiple) substitution

        MyWarn ("Unexpected ACTION_TYPE '$sub->{'ACTION_TYPE'}' for lookup '$name'.\n") if $sub->{'ACTION_TYPE'} ne 'g';

		if ($sub->{'FORMAT'} == 1)
        {
    		# Build $lhs and $rhs arrays:
    		$lhs = [ CoverToList ($sub->{'COVERAGE'}) ];
    		$rhs = [ map { $sub->{'RULES'}[$_][0]{'ACTION'} } (0 .. $#{$lhs}) ];
    
    		# Write out the substitution rules:
    		$start = 0;
    		while ($start <= $#{$lhs})
    		{
    			($last, $col, $lhsGroup, $rhsGroup) = GetGroupRange(@{$lhs}, @{$rhs}, $start);
    			$LookupText .= "SUB " . ($last > $start ? "GROUP \"" . $lhsGroup : "GLYPH \"" . NameFromID($lhs->[$start])) . "\"\n";
    			$LookupText .= "WITH " . join(" ", map {$_ == $col ? "GROUP \"$rhsGroup\"" : "GLYPH \"" . NameFromID($rhs->[$start][$_]) . "\"" } (0 .. $#{$rhs->[$start]}) ) . "\n";
    			$LookupText .= "END_SUB\n";
    			$start = $last+1;
 		    }
 		}
        else
		{
		    MyWarn "GSUB '$name' has unrecognized format $sub->{'FORMAT'}.\n" ;
		}
	}
	elsif ($l->{'TYPE'} == 3)
	{
		# 1-n (alternate) substitution

	    MyWarn ("Unexpected ACTION_TYPE '$sub->{'ACTION_TYPE'}' for lookup '$name'.\n") if $sub->{'ACTION_TYPE'} ne 'a';

		if ($sub->{'FORMAT'} == 1)
		{
    		$lhs = [ CoverToList ($sub->{'COVERAGE'}) ] ;
    		$rhs = [ map { $sub->{'RULES'}[$_][0]{'ACTION'} } (0 .. $#{$lhs}) ];
    
            # Write out the alternate substitution rules
    		for $start (0 .. $#{$lhs})
    		{
    		    for (0 .. $#{$rhs->[$start]})
    		    {
    		        $LookupText .= "SUB GLYPH \"" . NameFromID($lhs->[$start]) . "\"\nWITH GLYPH \"" . NameFromID($rhs->[$start][$_]) . "\"\nEND_SUB\n";
    		    }
    		}
    	}
        else
		{
		    MyWarn "GSUB '$name' has unrecognized format $sub->{'FORMAT'}.\n" ;
		}
	}
	elsif ($l->{'TYPE'} == 4)
	{
		# n-1 (ligature) substitution

	    MyWarn ("Unexpected ACTION_TYPE '$sub->{'ACTION_TYPE'}' for lookup '$name'.\n") if $sub->{'ACTION_TYPE'} ne 'g';

		if ($sub->{'FORMAT'} == 1)
        {
    		# First pick up the coverage list:
    		$cover = [ CoverToList ($sub->{'COVERAGE'}) ];
    
    		# Now build $lhs & $rhs:
    		
    		$lhs = [];
    		$rhs = [];
    
    		for $i (0 .. $#{$cover})
    		{
    			# For each first glyph in the coverage, there can be multiple match strings, each corresponding to a ligature:
    			foreach $subrule (@{$sub->{'RULES'}[$i]})
    			{
    				# Make sure this is really a n-1 mapping:
    				die "GSUB '$name' has invalid ligature mapping" if $#{$subrule->{'ACTION'}} > 0;
    
    				push @{$lhs}, [ $cover->[$i], @{$subrule->{'MATCH'}} ];
    				push @{$rhs}, ($gid = $subrule->{'ACTION'}[0]);
    
    				$g = $GlyphFromID{$gid};
    
    				# Set the number of components for the ligature in the glyph definition:
    				# $g->{'COMPONENTS'} = 1 + ($#{$subrule->{'MATCH'}} + 1) if $g->{'TYPE'} == "LIGATURE";
    			}
    		}
    
    		# Write out the substitution rules:
    		# (very similar to type 2, except $rhs and $lhs are reversed)
    		$start = 0;
    		while ($start <= $#{$rhs})
    		{
    			($last, $col, $rhsGroup, $lhsGroup) = GetGroupRange(@{$rhs}, @{$lhs}, $start);
    			$LookupText .= "SUB " . join(" ", map {$_ == $col ? "GROUP \"$lhsGroup\"" : "GLYPH \"" . NameFromID($lhs->[$start][$_]). "\"" } (0 .. $#{$lhs->[$start]}) ) . "\n";
    			$LookupText .= "WITH " . ($last > $start ? "GROUP \"" . $rhsGroup : "GLYPH \"" . NameFromID($rhs->[$start])) . "\"\n";
    			$LookupText .= "END_SUB\n";
    			$start = $last+1;
    		}
    	}
    	else
		{
		    MyWarn "GSUB '$name' has unrecognized format $sub->{'FORMAT'}.\n" ;
		}

	}
	
}

##########################
#
# Process GSUB lookup
#
# Appends one or more lookup definitions to $LookupText

sub ProcessGSUBLookup ($)		# GSUB lookup index to process
{
	my $lid = shift;	# lookup index
	my $l= $gsub->{'LOOKUP'}[$lid];		# lookup structure

	my $nsubs = scalar (@{$l->{'SUB'}});    # Number of subtables	
 
	foreach my $subtbl (0 .. $nsubs-1)
    {
        my $name = $l->{'-name'};
        $name .= "\\$subtbl" if $nsubs > 1;
   		my $sub = $l->{'SUB'}[$subtbl];
    
    	if ($l->{'TYPE'} <= 4)
    	{
            WriteLookupHeader ($l, $name);
        	$LookupText .= "IN_CONTEXT\nEND_CONTEXT\nAS_SUBSTITUTION\n";
    		WriteSimpleGSUBLookup ($l, $name, $subtbl);
  			$LookupText .= "END_SUBSTITUTION\n";
    	}
    
    	elsif ($l->{'TYPE'} == 6)		# Chaining context
    	{

    	    MyWarn ("Unexpected ACTION_TYPE '$sub->{'ACTION_TYPE'}' for lookup '$name'.\n") if $sub->{'ACTION_TYPE'} ne 'l';
    		
    		# For now, just handling format 3:
    		if ($sub->{'FORMAT'} == 3)
            {
    		    # All the gruntwork has been done by CoalesceChainContextSubtables()
        
        		# TODO: It is possible that the type 6 lookup we are processing has a coverage (MATCH) that is smaller than that
        		# specified by the target lookup. I don't think VOLT would construct such a thing, but some other tool might.
        		# We really should pick up the MATCH string and pass it to WriteSimpleGSUBLookup to do some
        		# error checking of some sort.

        		# OK, now we can emit the lookup
        
                WriteLookupHeader ($l, $name);
                $LookupText .= $sub->{'-context'};
            	$LookupText .= "AS_SUBSTITUTION\n";
            	if (defined $sub->{'-lookupID'})
            	{
            	    WriteSimpleGSUBLookup ($gsub->{'LOOKUP'}[$sub->{'-lookupID'}], $name, 0);
            	}
            	else
            	{
            	    MyWarn "Undefined target lookup in GSUB lookup '$name'\n";
            	}
      			$LookupText .= "END_SUBSTITUTION\n";
      		}
	    	else
    		{
    		    MyWarn "GSUB '$name' has unhandled format $sub->{'FORMAT'}.\n" ;
    		}

        }
        else
        {
    	    MyWarn ("Unhandled GSUB lookup $name (type $l->{'TYPE'}) ignored for now.\n");
    	}
    }
}


##########################
# CacheAP
#
# take a list of base glyphs with anchors, and marks with anchors, and
# determine an attachment point name. Cache the data in case it is re-used
#

my @APs;   # Array of cached AP info. Each item in array is a hash containing:
#
#   'bases' a hash, indexed by glyphID, returning array, indexed by component, returning ref to anchor.
#   'marks' a hash, indexed by glyphID, returning ref to anchor


sub CacheAP(\%\%)
{
    my ($bases, $marks) = @_;
    
    my ($i, $gid, $apname);
    
    my $found = -1;  
    
    for $i (0 .. $#APs)
    {
        $found = $i;    # Assume this one will work
        
        # See if this AP is acceptable by comparing our bases and marks with the ones in this AP
        # Check bases first
        foreach $gid (keys %{$bases})
        {
            # If this gid isn't in the AP, then we're OK so far
            next unless exists $APs[$i]->{'bases'}{$gid};
            # This gid is in the AP -- verify the x & y matches:
            # In order to pass the test, base must have the same number of components and each component's AP must match
            my $APbase = $APs[$i]->{'bases'}{$gid};
            my $base = $bases->{$gid};
            if ($#{$APbase} != $#{$base})
            {   
                # Not the same number of components
                $found = -1;
            }
            else
            {
                # Verify each component's ap matches
                foreach (0 .. $#{$base})
                {
                    next if $APbase->[$_]{'x'} == $base->[$_]{'x'} && $APbase->[$_]{'y'} == $base->[$_]{'y'};
                    $found = -1;
                    last;           # No need to keep looking at other components in this base
                }
            }
            last if $found == -1;   # No need to keep looking at other gids in this AP
        }
        next if $found == -1;   # This AP no good -- try the next one
        # Now check marks
        foreach $gid (keys %{$marks})
        {
            # If this gid isn't in the AP, then we're OK so far
            next unless exists $APs[$i]->{'marks'}{$gid};
            # This gid is in the AP -- verify the x & y matches:
            next if $APs[$i]->{'marks'}{$gid}{'x'} == $marks->{$gid}{'x'} && $APs[$i]->{'marks'}{$gid}{'y'} == $marks->{$gid}{'y'};
            $found = -1;
            last;
        }
        
        # At this point, if $found is still undef, the we've found a usable AP. 
        # However, if it is -1 then we need to keep looking
        
        next if $found == -1;   # This AP no good -- try the next one

        # All the bases and marks are either absent or they match the ones in this AP -- therefore ths AP can be used!
        last;
        
    }
    
    unless ($found >= 0)
    {
        # Create new, empty, AP cache entry
        push @APs, { 'bases' => {}, 'marks' => {} };
        $found = $#APs;
    }
    
    $apname = "attach$found";
    
    # Add any missing glyphs to the cache:
    # Record info both in the APCache and in the glyph structure (for building Anchor definitions)
    foreach $gid (keys %{$bases})
    {
        next if exists $APs[$found]->{'bases'}{$gid};
        $APs[$found]->{'bases'}{$gid} = $GlyphFromID{$gid}{'ANCHORS'}{$apname} = $bases->{$gid} ;
    }
    foreach $gid (keys %{$marks})
    {
        next if exists $APs[$found]->{'marks'}{$gid};
        $APs[$found]->{'marks'}{$gid} = $GlyphFromID{$gid}{'ANCHORS'}{"MARK_$apname"} = $marks->{$gid}; 
    }

    return $apname;
}    

##########################
# ValueRecord
#
# Return a VOLT source string for a Font::TTF::GPOS value record
#

my %vrKeys = ( XAdvance => 'ADV', YPlacement => 'DY', XPlacement => 'DX');
my @vrUnprocessed = ( qw( YAdvance XPlaDevice YPlaDevice XAdvDevice YAdvDevice XIdPlacement YIdPlacement));

sub ValueRecord(\%)
{
    my $rec = shift;
    my $res = "POS";
    for (sort keys %vrKeys)
    {
        $res .= " $vrKeys{$_} $rec->{$_}" if exists $rec->{$_} && $rec->{$_} != 0;
    }
    $res .= " END_POS";

    foreach (@vrUnprocessed)
    {
        if (defined $rec->{$_})
        {
            MyWarn ("Unhandled ValueRecord data '$_'\n");
        }
    }
    
    $res;
}
        
##########################
# PairKern
#
# Returns a VOLT source string for the adjustment between two glyphs in a GPOS pairadjust (type 2) lookup of format 1
#
sub PairKern($$$)
{
    my ($sub, $first, $second) = @_;    # ref to sub table, first GID, second GID
    
    # Return it if already computed
    return $sub->{'PKcache'}{"$first.$second"} if exists $sub->{'PKcache'}{"$first.$second"};  
    
    my $res;
    
    # Find the rule, if it exists:
    
    if (exists $sub->{'COVERAGE'}{'val'}{$first})
    {
        my $rule = $sub->{'RULES'}[$sub->{'COVERAGE'}{'val'}{$first}];
        foreach my $subrule (@{$rule})
        {
            next unless $subrule->{'MATCH'}[0] == $second;
            # Found it!
            $res = ValueRecord(%{$subrule->{'ACTION'}[0]}) . ' ' . ValueRecord(%{$subrule->{'ACTION'}[1]});
            last;
        }
    }
    $res = undef if $res eq 'POS END_POS POS END_POS';  # All zero -- make as if this wasn't even present.
    $sub->{'PKcache'}{"$first.$second"} = $res;
}
        
        

##########################
# WriteSimplePOSLookup()
#
# Routine to append a simple GPOS lookup (types 1 - 6) subtable to $LookupText
#

sub WriteSimpleGPOSLookup ($$$)		# lookup structure reference, name to be used in lookup, subtable index
{
	my ($l, $name, $stbl) = @_;

	my ($lhs, $rhs, $start, $last, $col, $lhsGroup, $rhsGroup);
	my (@cover, $gid, $i, $subrule);
	
	my $sub = $l->{'SUB'}[$stbl];

	if ($l->{'TYPE'} == 1)
	{
	    # Single adjustment

	    MyWarn ("Unexpected ACTION_TYPE '$sub->{'ACTION_TYPE'}' for lookup '$name'.\n") if $sub->{'ACTION_TYPE'} !~ /[ov]/;

	    
		@cover = CoverToList ($sub->{'COVERAGE'}) ;
		
		
		if ($sub->{'FORMAT'} == 1)
		{
		    # Format 1 -- a single value record for entire group.
		    
    		$LookupText .= "ADJUST_SINGLE " . GlyphOrGroup(@cover) . " BY " . ValueRecord(%{$sub->{'ADJUST'}}) . "\nEND_ADJUST\n";
    	}
    	elsif ($sub->{'FORMAT'} == 2)
    	{
    	    # Format 2 -- value record for each covered glyph.
    	    $LookupText .= "ADJUST_SINGLE\n";
    	    
    	    foreach (0 .. $#cover)
    	    {
    	        $LookupText .= " GLYPH \"" . NameFromID($cover[$_]) . "\" BY " . ValueRecord(%{$sub->{'RULES'}[$_][0]{'ACTION'}[0]}) . "\n";
    	    }
    	    $LookupText .= "END_ADJUST\n";
    	}
    	else
        {
		    MyWarn "GPOS '$name' has unrecognized format $sub->{'FORMAT'}.\n"; 
		}
	}
	elsif ($l->{'TYPE'} == 2)
	{
	    # Pair adjustment

	    MyWarn ("Unexpected ACTION_TYPE '$sub->{'ACTION_TYPE'}' for lookup '$name'.\n") if $sub->{'ACTION_TYPE'} != 'p';

		$LookupText .= "ADJUST_PAIR\n";

        if ($sub->{'FORMAT'} == 1)
        {
    		# Format 1 -- glyph-pair value records. 
    		
    		# This is a complicated one because I want to coalesce glyphs into groups. I do this by examining every
    		# pair of first glyphs and checking their adjustment with all 2nd glyphs -- if these adjustments are
    		# all the same then the two first glyphs can be grouped together. Similarly for every pair of 2nd 
    		# glyphs (examine the adjustment with all possible first glyphs).
    		
		    my ($i, $j, $k);  
		    

    		# First step is to get a complete list of 1st and 2nd glyphs:
    		
    		my @first = CoverToList($sub->{'COVERAGE'});
    		my %second;
		    foreach $i (@{$sub->{'RULES'}})
		    {
		        foreach $j (@{$i})
		        {
		            $second{$j->{'MATCH'}[0]} = 1;
		        }
		    }
		    my @second = sort {$a <=> $b} keys %second;
		    
		    # OK, the fun begins
		    my (@g1, @g2);  # record of the results. These arrays parallel @first and @second, and contain:
		    #   undef -- this glyph hasn't matched anything yet.
		    #   scalar -- index of a (prior) glyph with which this one can be grouped
		    #   array -- list of indicies of (subsequent) glyphs which are grouped with this one.
		    
		    # First examine every pair of 1st glyphs to see if they can coalesce
		    for $i (0 .. $#first)
		    {
		        next if defined $g1[$i];
		        # Initialize array in case we get any that can coalesce with it
		        $g1[$i] = [ $i ];
		        for $j ($i+1 .. $#first)
		        {
		            next if defined $g1[$j];
		            # for first glyphs at $i1 and $j1, compare adjustment with all 2nd glyphs
		            my $match = 1;  # assume they all match
		            for $k (0 .. $#second)
		            {
		                if (PairKern($sub, $first[$i], $second[$k]) ne PairKern($sub, $first[$j], $second[$k]))
		                {
		                    $match = 0; #fail
		                    last;
		                }
		            }
		            next unless $match;
		            # Found a match! We can coalesce First glyphs indexed by $i and $j
		            push @{$g1[$i]}, $j;
		            $g1[$j] = $i;
		        }
		    }
		    # Make sure groups are big enough to use:
		    if (defined $opt_g)
		    {
		        for $i (0 .. $#first)
		        {
		            next unless ref($g1[$i]) eq 'ARRAY';
		            next if scalar(@{$g1[$i]}) >= $opt_g;   # This is a keeper
		            next if scalar(@{$g1[$i]}) == 1;        # This is already a singleton -- leave it.
		            # Group is too small -- destroy it:
		            foreach $j (@{$g1[$i]})
		            {
		                $g1[$j] = [ $j ];
		            }
		        }
		    }
		    
		    # Now repeat the logic on the every pair of 2nd glyphs
		    for $i (0 .. $#second)
		    {
		        next if defined $g2[$i];
		        # Initialize array in case we get any that can coalesce with it
		        $g2[$i] = [ $i ];
		        for $j ($i+1 .. $#second)
		        {
		            next if defined $g2[$j];
		            # for second glyphs at $i1 and $j1, compare adjustment with all first glyphs
		            my $match = 1;  # assume they all match
		            for $k (0 .. $#first)
		            {
		                if (PairKern($sub, $first[$k], $second[$i]) ne PairKern($sub, $first[$k], $second[$j]))
		                {
		                    $match = 0; #fail
		                    last;
		                }
		            }
		            next unless $match;
		            # Found a match! We can coalesce Second glyphs indexed by $i and $j
		            push @{$g2[$i]}, $j;
		            $g2[$j] = $i;
		        }
		    }
		    # Make sure groups are big enough to use:
		    if (defined $opt_g)
		    {
		        for $i (0 .. $#second)
		        {
		            next unless ref($g2[$i]) eq 'ARRAY';
		            next if scalar(@{$g2[$i]}) >= $opt_g;   # This is a keeper
		            next if scalar(@{$g2[$i]}) == 1;        # This is already a singleton -- leave it.
		            # Group is too small -- destroy it:
		            foreach $j (@{$g2[$i]})
		            {
		                $g2[$j] = [ $j ];
		            }
		        }
		    }
		    
		    # Whew!  Now build the lookup
		    map { $LookupText .= ' FIRST ' . GlyphOrGroup(@first[@{$_}]); } grep {ref($_) eq 'ARRAY'} @g1;
		    $LookupText .= "\n";
		    map { $LookupText .= ' SECOND ' . GlyphOrGroup(@second[@{$_}]); } grep {ref($_) eq 'ARRAY'} @g2;
		    $LookupText .= "\n";
		    
		    $i = 1;
		    for my $f (0 .. $#first)
		    {
		        next unless ref($g1[$f]) eq 'ARRAY';
		        $j = 1;
    		    for my $s (0 .. $#second)
    		    {
    		        next unless ref($g2[$s]) eq 'ARRAY';
    		        my $res = PairKern($sub, $first[$g1[$f][0]], $second[$g2[$s][0]]);
    		        $LookupText .= " $i $j BY $res\n" if defined $res;
    		        $j++;
    		    }
    		    $i++;
		    }
		
	    }
	    elsif ($sub->{'FORMAT'} == 2)
		{
    		# Format 2 -- class-based value records
    		
    		my @first = map { $#{$_} >= 0 ? ' FIRST ' . GlyphOrGroup(@{$_}) : undef } ClassToLists ($sub->{'CLASS'});
    		my @second = map { $#{$_} >= 0 ? ' SECOND ' . GlyphOrGroup(@{$_}) : undef } ClassToLists ($sub->{'MATCH'}[0]);
    		
    		$LookupText .= join('', @first) . "\n";
    		$LookupText .= join('', @second) . "\n";
    		my $i = 1;
    		foreach my $f (0 .. $#first)
    		{
                next unless defined $first[$f];
                my $j = 1;
                foreach my $s (0 .. $#second)
                {
                    next unless defined $second[$s];
                    $LookupText .= " $i $j BY " . ValueRecord(%{$sub->{'RULES'}[$f][$s]{'ACTION'}[0]}) . ' ' . ValueRecord(%{$sub->{'RULES'}[$f][$s]{'ACTION'}[1]}) . "\n";
                    $j++;
                }
                $i++;
            }
        }
        else
        {
		    MyWarn "GPOS '$name' has unrecognized format $sub->{'FORMAT'}.\n"; 
		}

        $LookupText .= "\nEND_ADJUST\n";	

	}
	elsif ($l->{'TYPE'} == 3)
	{
	    # Cursive Attachment

	    MyWarn ("Unexpected ACTION_TYPE '$sub->{'ACTION_TYPE'}' for lookup '$name'.\n") if $sub->{'ACTION_TYPE'} ne 'e';
	    
		if ($sub->{'FORMAT'} == 1)
		{
    		# First pick up the coverage list:
    		@cover = CoverToList ($sub->{'COVERAGE'}) ;
    		
    		# Initialize hashes to keep track of GIDs with entry/exit points
    		$lhsGroup = {};
    		$rhsGroup = {};
    		
    		# Pick out anchor definitions, building left-hand and right-hand lists of glyphs
    		foreach my $gid (@cover)
    		{
    		    my $action = $sub->{'RULES'}[$sub->{'COVERAGE'}{'val'}{$gid}][0]{'ACTION'};
    		    
    		    for $i (0 .. 1)
    		    {
    		        next unless $action->[$i];
    		        # Entry or exit defined:
    		        my $which = (qw(entry exit))[$i];
    		        $GlyphFromID{$gid}->{'ANCHORS'}{$which} = $action->[$i];      # ref to anchor -- we'll parse this apart later
    		        ($i == 0 ? $rhsGroup : $lhsGroup)->{$gid} = 1;    # Build lhs & rhs groups
    		    }
    		}
    		$LookupText .= "ATTACH_CURSIVE\n";
    		$LookupText .= "EXIT  " . GlyphOrGroup(keys %{$lhsGroup}) . "\n";
    		$LookupText .= "ENTER  " . GlyphOrGroup(keys %{$rhsGroup}) . "\n";
    		$LookupText .= "END_ATTACH\n";
    	}
    	else
		{
		    MyWarn "GPOS '$name' has unrecognized format $sub->{'FORMAT'}.\n"; 
		}
    	
    }
    elsif ($l->{'TYPE'} == 4 || $l->{'TYPE'} == 5 || $l->{'TYPE'} == 6)
    {
        # Mark-to-Base and Mark-to-Mark (identical in structure)
        
        my (@marks, @bases);    # Indexed by markclass
        
        my ($class, $anchor, $rules, $component);

	    MyWarn ("Unexpected ACTION_TYPE '$sub->{'ACTION_TYPE'}' for lookup '$name'.\n") if $sub->{'ACTION_TYPE'} ne 'a';
	    
		if ($sub->{'FORMAT'} == 1)
        {
    	    foreach $gid (keys %{$sub->{'MATCH'}[0]{'val'}})
    	    {
    	        ($class, $anchor) = @{$sub->{'MARKS'}[$sub->{'MATCH'}[0]{'val'}{$gid}]};
    	        $marks[$class]{$gid} = $anchor;
    	    }
    	    
    	    # Programmer note: indexing scheme for RULES is:
    	    #   $sub->{'RULES'}[Base_char_index][Ligature_component]{'ACTION'}[Mark_class]
    	    
    	    foreach $gid (keys %{$sub->{'COVERAGE'}{'val'}})
    	    {
    	        my $rules = $sub->{'RULES'}[ $sub->{'COVERAGE'}{'val'}{$gid} ];
    	        if ($#{$rules} > 0)
    	        {
    	            # Real ligature -- let's make sure glyph data knows how many components
    	            MyWarn "Lookup $name defines different number of components for ligature glyph $gid.\n" 
    	                if exists $GlyphFromID{$gid}->{'COMPONENTS'} and $GlyphFromID{$gid}->{'COMPONENTS'} != $#{$rules} + 1;
    	            $GlyphFromID{$gid}->{'COMPONENTS'} = $#{$rules} + 1 ;
    	        }
    	        foreach $component (0 .. $#{$rules})
    	        {
                    $class = 0;
                    foreach $anchor (@{$sub->{'RULES'}[ $sub->{'COVERAGE'}{'val'}{$gid} ][$component]{'ACTION'}})
                    {
                        $bases[$class]{$gid}[$component] = $anchor if defined $anchor;    
                        $class++;
                    }
    	        }
    	    }
    	    
       		$LookupText .= "ATTACH " . GlyphOrGroup(keys %{$sub->{'COVERAGE'}{'val'}}) . "\nTO";
    	    
    	    for $class (0 .. $#marks)
    	    {
    	        next unless scalar(keys %{$marks[$class]});  # ignore any mark classes that have no elements
    	        my $APname = CacheAP(%{$bases[$class]}, %{$marks[$class]});
        		$LookupText .= " " . GlyphOrGroup(keys %{$marks[$class]}) . " AT ANCHOR \"$APname\"";
    	    }    
       		$LookupText .= "\nEND_ATTACH\n";
       	}
       	else
		{
		    MyWarn "GPOS '$name' has unrecognized format $sub->{'FORMAT'}.\n"; 
		}

    
    
    }
	else
	{
	    MyWarn ("Unhandled GPOS lookup $name (type $l->{'TYPE'}) ignored for now.\n");
	}
}



##########################
#
# Process GPOS lookup
#
# Appends one or more lookup definitions to $LookupText

sub ProcessGPOSLookup ($)		# GPOS lookup index to process
{
	my $lid = shift;	# lookup index
	my $l= $gpos->{'LOOKUP'}[$lid];		# lookup structure
	
	
	my $nsubs = scalar (@{$l->{'SUB'}});    # Number of subtables
 
	foreach my $subtbl (0 .. $nsubs-1)
    {
        my $name = $l->{'-name'};
        $name .= "\\$subtbl" if $nsubs > 1;
    
    	if ($l->{'TYPE'} <= 6)
    	{
            WriteLookupHeader ($l, $name);
        	$LookupText .= "IN_CONTEXT\nEND_CONTEXT\nAS_POSITION\n";
    		WriteSimpleGPOSLookup ($l, $name, $subtbl);
  			$LookupText .= "END_POSITION\n";
    	}
    	elsif ($l->{'TYPE'} == 8)		# Chaining context
    	{
    		my $sub = $l->{'SUB'}[$subtbl];
    		
    		# This is just like GSUB:
    		if ($sub->{'FORMAT'} == 3)
    		{
    		    # All the gruntwork has been done by CoalesceChainContextSubtables()
        
        		# TODO: It is possible that the type 6 lookup we are processing has a coverage (MATCH) that is smaller than that
        		# specified by the target lookup. I don't think VOLT would construct such a thing, but some other tool might.
        		# We really should pick up the MATCH string and pass it to WriteSimpleLookup to do some
        		# error checking of some sort.
        
        		# OK, now we can emit the lookup
        
                WriteLookupHeader ($l, $name);
            	$LookupText .= $sub->{'-context'};
            	$LookupText .= "AS_POSITION\n";
            	if (defined $sub->{'-lookupID'})
            	{
            	    WriteSimpleGPOSLookup ($gpos->{'LOOKUP'}[$sub->{'-lookupID'}], $name, 0);
            	}
            	else
            	{
            	    MyWarn "Undefined target lookup in GSUB lookup '$name'\n";
            	}
      			$LookupText .= "END_POSITION\n";
      		}
      		else
        	{
		        MyWarn "GPOS '$name' has unhandled format $sub->{'FORMAT'}.\n"; 
		    }  
        }
        else
        {
    	    MyWarn ("Unhandled GPOS Lookup $name (type $l->{'TYPE'}) ignored for now.\n");
    	}
    }
}



##########################
#
# Generate the script, lang, and feature source. 
# This also tells us which lookups need to be directly processed.
#

my ($s, $l, $f);    # Script, Lang, and Feature

# Record which lookups needed within gsub and gpos
$gsub->{'-LookupsNeeded'} = []; 
$gpos->{'-LookupsNeeded'} = [];


# Note: VOLT co-mingles script/lang/feature trees from GPOS and GDEF.
# This code attempts to keep script, lang, and feature lists sorted by tag, though this isn't essential for VOLT

# Compile a list of scripts, then iterate over them
my %scripts;
map {$scripts{$_} = 1} (keys %{$gsub->{'SCRIPTS'}}) if $gsub;
map {$scripts{$_} = 1} (keys %{$gpos->{'SCRIPTS'}}) if $gpos;
foreach $s (sort keys %scripts)
{
	$SLFText .= "DEF_SCRIPT ";
	$SLFText .= "NAME \"$ttnames{'SCRIPT'}{$s}\" " if defined $ttnames{'SCRIPT'}{$s};
	$SLFText .= "TAG \"$s\"\n\n";

    # For this script, compile list of languages and then iterate over them
    my %langs;
    map {$langs{$_} = 1} (@{$gsub->{'SCRIPTS'}{$s}{'LANG_TAGS'}}) if $gsub;
    map {$langs{$_} = 1} (@{$gpos->{'SCRIPTS'}{$s}{'LANG_TAGS'}}) if $gpos;
    foreach $l ('DEFAULT', sort keys %langs)
	{
	    # Some fonts, e.g., Doulos, have a default language *and* have a 'dflt' language, one referring to the other. Skip one if so:
		next if ($l eq 'DEFAULT' || $l eq 'dflt') && (exists $gsub->{'SCRIPTS'}{$s}{$l}{' REFTAG'} || exists $gpos->{'SCRIPTS'}{$s}{$l}{' REFTAG'});
		
		$SLFText .= "DEF_LANGSYS ";
		if ($l eq 'DEFAULT')
		{
			$SLFText .= "NAME \"Default\" TAG \"dflt\"\n\n";
		}
		else
		{
			$SLFText .= "NAME \"$ttnames{'LANGUAGE'}{$l}\" " if defined $ttnames{'LANGUAGE'}{$l};
			$SLFText .= "TAG \"$l\"\n\n";
		}

		MyWarn ("VOLT doesn't understand default GSUB feature; setting ignored ($s/$l).\n") if ($gsub && $gsub->{'SCRIPTS'}{$s}{$l}{'DEFAULT'} != 65535);
		MyWarn ("VOLT doesn't understand default GPOS feature; setting ignored ($s/$l).\n") if ($gpos && $gpos->{'SCRIPTS'}{$s}{$l}{'DEFAULT'} != 65535);

        # For this script and language, compile list of features and iterate over them
        
        # Note: there can be multiple instances of any given feature (e.g., "ccmp" then "ccmp _1", etc.)
        # because the same feature tag can be (probably is) present under each language. However, VOLT
        # knows them all as simply the 4 character tag (e.g., "ccmp")
        
        my %feats;
        # It is possible, but not likely, that we'll have a feature implemented by both gsub and gpos lookups.
        # Therefore we use a slightly different technique than above to record what features are needed:
        map {$feats{substr($_,0,4)} .= "|S$_" } (@{$gsub->{'SCRIPTS'}{$s}{$l}{'FEATURES'}} ) if $gsub;
        map {$feats{substr($_,0,4)} .= "|P$_"} (@{$gpos->{'SCRIPTS'}{$s}{$l}{'FEATURES'}} ) if $gpos;
        foreach $f (sort keys %feats)
		{
			$SLFText .= "DEF_FEATURE ";
			$SLFText .= "NAME \"$ttnames{'FEATURE'}{$f}\" " if defined $ttnames{'FEATURE'}{$f};
			$SLFText .= "TAG \"$f\"\n";
			
			foreach ( $feats{$f} =~ /\|([^|]+)/g)
			{
			    my ($which, $actualfeat) = ($_ =~ /^(.)(.*)$/);
			    # At this point $which is either 'S' or 'P' (for gsub or gpos), and 
			    # $actualfeat is the full feature name, e.g., "ccmp _1".
			    
			    my $tbl = ($which eq 'S' ? $gsub : $gpos);
			    foreach my $lid (@{$tbl->{'FEATURES'}{$actualfeat}{'LOOKUPS'}})
			    {
                    unless ($tbl->{'-LookupsNeeded'}[$lid])
                    {
                        # One time processing for this lookup:
                        
    			        # Make sure we remember to include this one in DEF_LOOKUPs:
    			        $tbl->{'-LookupsNeeded'}[$lid] = 1;
    			        
    			        # Give it a name:
    			        $tbl->{'LOOKUP'}[$lid]{'-name'} = "$f" . "_$which$lid";
    			        
    			        # If this is a chaining context lookup, we need to coalesce subtables.
    			        CoalesceChainContextSubtables ($tbl->{'LOOKUP'}[$lid],$tbl) if $tbl->{'LOOKUP'}[$lid]{'TYPE'} == ($which eq 'S' ? 6 : 8);
    			    }
   			        my $nsubs = scalar (@{$tbl->{'LOOKUP'}[$lid]{'SUB'}});    # Number of subtables
   			        my $lname = $tbl->{'LOOKUP'}[$lid]{'-name'};
			        foreach (0 .. $nsubs-1)
			        {
			            $SLFText .= " LOOKUP \"$lname" . ($nsubs > 1 ? "\\$_" : '') . "\"";
			        }
			    }
			    
			}

			$SLFText .= "\nEND_FEATURE\n";
		}
		$SLFText .= "END_LANGSYS\n";
	}
	$SLFText .= "END_SCRIPT\n";
}


# Now we know what lookups we need to process, let's do it:
foreach my $i (0 .. $#{$gsub->{'-LookupsNeeded'}})
{
    ProcessGSUBLookup $i if $gsub->{'-LookupsNeeded'}[$i];
}
    
foreach my $i (0 .. $#{$gpos->{'-LookupsNeeded'}})
{
    ProcessGPOSLookup $i if $gpos->{'-LookupsNeeded'}[$i];
}


# OK, now we can collect everything together

my $vtp;

# print GLYPH definitions:
foreach $gid (sort {$a <=> $b} keys %GlyphFromID)
{
	$g = $GlyphFromID{$gid};
	$vtp .= "DEF_GLYPH \"" . NameFromID($gid) . "\" ID $gid";

	if (exists $g->{'@UNICODES'}) {
		# If array contains exactly one value, output UNICODE in gdef, else must output UNICODEVALUES
		if (scalar (@{$g->{'@UNICODES'}}) == 1) {
			$vtp .= sprintf (" UNICODE %d", $g->{'@UNICODES'}[0]);
		} else {
			$vtp .= sprintf (" UNICODEVALUES \"%s\"", join (",", map {sprintf "U+%04X", $_} @{$g->{'@UNICODES'}}));
		}
	}

	$vtp .= " TYPE " . (exists $g->{'TYPE'} ? $g->{'TYPE'} : "BASE");
	if ($g->{'TYPE'} eq "LIGATURE")
	{
		if (exists $g->{'COMPONENTS'})
		{
			$vtp .= " COMPONENTS $g->{'COMPONENTS'}" ;
		}
		else
		{
			MyWarn ("Glyph $gid is a ligature, but I don't know how many components it has.\n");
		}
	}

	$vtp .= " END_GLYPH\n";

}

# print Script/Language/Features:
$vtp .=  $SLFText;

# print  GROUP definitions:
foreach $g (GetGroupNames)
{
	$vtp .= "DEF_GROUP \"$g\"\n ENUM";
	map {$vtp .= " GLYPH \"$GlyphFromID{$_}->{'NAME'}\"" } GetGroup($g);
	$vtp .= " END_ENUM\nEND_GROUP\n";
}

# print lookups:
$vtp .= $LookupText;

# print anchors
foreach $gid (sort {$a <=> $b} keys %GlyphFromID)
{
    my $g = $GlyphFromID{$gid};
    
    foreach my $apname (sort keys %{$g->{'ANCHORS'}})
    {
        my ($anchors, $component);
        
        if (ref $g->{'ANCHORS'}{$apname} eq 'ARRAY')
        {   $anchors = $g->{'ANCHORS'}{$apname}; }
        else
        {   $anchors = [ $g->{'ANCHORS'}{$apname} ];}
        
        for $component (1 .. $#{$anchors}+1)
        {
            my $anchor = $anchors->[$component-1];
            for (qw(xid yid p xdev ydev))
            {
                MyWarn "Glyph anchor field '$_' not implemented -- being ignored.\n" if exists $anchor->{$_};
            }
            $vtp .= "DEF_ANCHOR \"$apname\" ON $gid GLYPH ". NameFromID($gid) . " COMPONENT $component LOCKED AT POS DX $anchor->{'x'} DY $anchor->{'y'} END_POS END_ANCHOR\n";
        }
    }
}

# print PPM text:
$vtp .= "GRID_PPEM 20\nPRESENTATION_PPEM 72\nPPOSITIONING_PPEM 144";

# print cmap info:
foreach (sort { $a->{'Platform'} <=> $b->{'Platform'} } @{$font->{'cmap'}{'Tables'}})
{
	$vtp .= "\nCMAP_FORMAT $_->{'Platform'} $_->{'Encoding'} $_->{'Format'} ";
}
$vtp .= "END\n";


# Write out results

if (defined $ARGV[1])
{
    # Write out new font:
    
    # Insert the replacement source into the font
    # Create, if it doesn't exist, the VOLT source table we are going to insert
    $font->{'TSIV'} = Font::TTF::Table->new (PARENT => $font, NAME => 'TSIV') unless exists $font->{'TSIV'};
    
    # Replace source:
    $font->{'TSIV'}->{' dat'} = $vtp;
    
    # Remove other VOLT tables if they exist:
    for (qw( TSID TSIP TSIS )) { delete $font->{$_} };
    
    unless ($opt_r)
    {
    	# Remove compiled tables if they exist:
    	for (qw( GDEF GPOS GSUB)) { delete $font->{$_} };
    }
    
    $font->out($ARGV[1]);
}

if (defined $opt_v)
{
	# Open output source file:
	open (OUT, ">$opt_v") or die "Couldn't open '$opt_v' for writing.";
	print OUT $vtp;
	close OUT;
}

my $xx;
$xx = "\nFINISHED. ";
$xx .= ($warningCount > 0 ? $warningCount : "No") . " warning" . ($warningCount == 1 ? '' : 's') . " issued. ";
$xx .= ($genericCount > 0 ? $genericCount : "No") . " unnamed glyph" . ($genericCount == 1 ? '' : 's') . " used. \n\n";
if ($opt_l)
{
    print LOG $xx;
    close LOG;
}
print STDERR $xx;

#])}