texlive[57171] trunk: fontools (19dec20)

commits+karl at tug.org commits+karl at tug.org
Sat Dec 19 22:26:11 CET 2020


Revision: 57171
          http://tug.org/svn/texlive?view=revision&revision=57171
Author:   karl
Date:     2020-12-19 22:26:11 +0100 (Sat, 19 Dec 2020)
Log Message:
-----------
fontools (19dec20)

Modified Paths:
--------------
    trunk/Build/source/texk/texlive/linked_scripts/fontools/afm2afm
    trunk/Build/source/texk/texlive/linked_scripts/fontools/autoinst
    trunk/Build/source/texk/texlive/linked_scripts/fontools/ot2kpx
    trunk/Master/texmf-dist/doc/man/man1/afm2afm.1
    trunk/Master/texmf-dist/doc/man/man1/afm2afm.man1.pdf
    trunk/Master/texmf-dist/doc/man/man1/autoinst.1
    trunk/Master/texmf-dist/doc/man/man1/autoinst.man1.pdf
    trunk/Master/texmf-dist/doc/man/man1/ot2kpx.1
    trunk/Master/texmf-dist/doc/man/man1/ot2kpx.man1.pdf
    trunk/Master/texmf-dist/doc/support/fontools/splitttc
    trunk/Master/texmf-dist/scripts/fontools/afm2afm
    trunk/Master/texmf-dist/scripts/fontools/autoinst
    trunk/Master/texmf-dist/scripts/fontools/ot2kpx

Modified: trunk/Build/source/texk/texlive/linked_scripts/fontools/afm2afm
===================================================================
--- trunk/Build/source/texk/texlive/linked_scripts/fontools/afm2afm	2020-12-19 21:25:38 UTC (rev 57170)
+++ trunk/Build/source/texk/texlive/linked_scripts/fontools/afm2afm	2020-12-19 21:26:11 UTC (rev 57171)
@@ -37,7 +37,7 @@
 use Getopt::Long;
 use Pod::Usage;
 
-my $VERSION = "20200729";
+my $VERSION = "20201218";
 
 parse_commandline();
 
@@ -421,7 +421,7 @@
 
 =head1 VERSION
 
-This document describes B<afm2afm> version 20200729.
+This document describes B<afm2afm> version 20201218.
 
 
 =head1 RECENT CHANGES

Modified: trunk/Build/source/texk/texlive/linked_scripts/fontools/autoinst
===================================================================
--- trunk/Build/source/texk/texlive/linked_scripts/fontools/autoinst	2020-12-19 21:25:38 UTC (rev 57170)
+++ trunk/Build/source/texk/texlive/linked_scripts/fontools/autoinst	2020-12-19 21:26:11 UTC (rev 57171)
@@ -40,7 +40,7 @@
 use Pod::Usage ();
 use POSIX ();
 
-my $VERSION = '20200729';
+my $VERSION = '20201218';
 
 my ($d, $m, $y) = (localtime time)[3 .. 5];
 my $TODAY = sprintf "%04d/%02d/%02d", $y + 1900, $m + 1, $d;
@@ -63,10 +63,10 @@
 
         Log         Logs the font parsing and creation process.
 
-        Font        Code for getting font info. Contains two subpackages:
+        Attr        Contains tables and routines that represent
+                    font attributes (weight, width, shape).
 
-                    Font::Raw       Gets 'raw' data from font files.
-                    Font::Info      Extracts font info from raw data.
+        Font        Parses an OpenType font's metadata.
 
         Tables      Contains tables that drive the font creation process
                     (especially the decisions which fonts to create).
@@ -73,11 +73,6 @@
 
         Work        Generates all font files, driven by data from `Tables`.
 
-        NFSS        Contains tables and routines that map
-                    font characteristics (weight, width, shape)
-                    to NFSS attributes. This data is also used by
-                    `Font::Info`, to avoid duplication.
-
         LaTeX       Creates LaTeX support (.sty and .fd files).
 
         Otftotfm    Drives `otftotfm` to actually generate the fonts.
@@ -99,7 +94,7 @@
 
     my %fontfamily;
     for my $fontfile (@ARGV) {
-        my $font = Font::get_fontinfo($fontfile);
+        my $font = Font::parse($fontfile);
         my $family = $font->{family};
         push @{$fontfamily{$family}}, $font;
     }
@@ -117,8 +112,9 @@
         # until after the results of the parsing have been logged.
         Font::assert_unique($log, $fontlist);
 
-        my $nfss_mapping = NFSS::map_nfss_codes($fontlist);
-        $log->log_nfss_mapping($nfss_mapping);
+        my $mappings = Attr::map_nfss_codes($fontlist);
+        my ($from_nfss, $to_nfss) = @{$mappings};
+        $log->log_nfss_mapping($from_nfss);
 
         my @workitems = Work::generate_worklist($fontlist);
         $log->log_worklist(\@workitems);
@@ -129,8 +125,7 @@
         $log->log_commands(\@commands) if $ARGV{verbose} >= 1;
 
         if (!$ARGV{dryrun}) {
-            $nfss_mapping = NFSS::invert_mapping($nfss_mapping);
-            LaTeX::create_support_files(\@workitems, $family, $nfss_mapping);
+            LaTeX::create_support_files(\@workitems, $family, $to_nfss);
             Otftotfm::run_commands(\@commands, $family, $log);
 
             if ($ARGV{t1suffix}) {
@@ -154,94 +149,401 @@
 ############################################################################
 
 
-package Font;
+package Attr;
 
+=begin Comment
+
+    Some fontnames contain abbreviated words for width, weight and/or shape;
+    we unabbreviate these using the following table.
+
+=end Comment
+
+=cut
+
+my %FULL_FORM = (
+    cmp     =>  'compressed',
+    comp    =>  'compressed',
+    cond    =>  'condensed',
+    demi    =>  'demibold',
+    extcond =>  'extracondensed',
+    hair    =>  'hairline',
+    incline =>  'inclined',
+    it      =>  'italic',
+    ita     =>  'italic',
+    md      =>  'medium',
+    slant   =>  'slanted',
+    ultra   =>  'ultrablack',
+);
+
+# Auxiliary table that reverses FULL_FORM; maps full forms to abbrevs.
+my %abbrev;
+while (my ($k, $v) = each %FULL_FORM) {
+    push @{$abbrev{$v}}, $k;
+}
+
+# Add full forms as known abbreviations of themselves.
+for my $full (keys %abbrev) {
+    push @{$abbrev{$full}}, $full;
+}
+
+# Internal helper: returns a list of known abbreviations of its argument.
+sub _get_abbreviated_forms {
+    my $key = shift;
+
+    return @{ $abbrev{$key} // [$key] };
+}
+
+
+=begin Comment
+
+    LaTeX's NFSS contains a number of standard codes for weight and width:
+    - weight: ul, el, l, sl, m, sb, b, eb, ub
+    - width:  uc, ec, c, sc, m, sx, x, ex, ux
+
+    These codes are not always a perfect match with the weights and widths
+    present in a font family; some families (especially many sans serif ones)
+    contain more or different weights and widths, and the naming of those
+    weights and widths isn't always consistent between font families.
+    To handle this situation, we use a two-tiered approach:
+    1.  We install all fonts using a "series" name that is the concatenation
+        of whatever the font designer has chosen to call the weight and width
+        (but in all *lower*case).
+    2.  We add "alias" rules to the .fd files that map the standard NFSS codes
+        to actual fonts.
+
+    In step 1, we follow NFSS in leaving out any occurrence of
+    the word "regular" unless *both* weight and width are Regular;
+    in that case, the 'series' attribute becomes "regular".
+
+    The two tables WEIGHT and WIDTH are used to control step 2.
+    It contains several entries of the form
+
+        sc  =>  [ qw( semicondensed narrow ) ],
+
+    This should be read as follows: the NFSS code "sc" is mapped to
+    the *first* width on the right hand side present in the current family.
+
+    Please note that the tables contain empty keys instead of "m" for the
+    regular weight and width. NFSS actually combines weight and width into
+    a single "series" attribute; a weight or width of "m" is left out of
+    this combination (unless *both* weight and width are equal to "m"; then
+    the series becomes "m", but that's a special case we deal with later on).
+
+    In addition to the mapping of NFSS codes, the two mentioned tables are
+    also used in parsing the font's metadata to determine its weight and
+    width: any string that occurs on the right hand side is considered a
+    possible name to be searched for.
+
+    These tables can be extended to teach autoinst about new weights or
+    widths.  Suppose your font family contains a "Hemibold" weight, that
+    you want mapped to the "sb" code. Then add the name "hemibold" to
+    the right hand side of the "sb" entry in the WEIGHT table:
+
+        sb  =>  [ qw( semibold demibold medium hemibold ) ],
+
+    In this case, since it's in last position, it's only mapped to "sb"
+    if none of the other fonts are present. Put it earlier in the list
+    to give it higher priority.
+
+    Note that autoinst converts all metadata to lowercase to avoid
+    inconsistent capitalization; so all entries in these tables should
+    be *lowercase* as well.
+
+    Technical notes:
+    -   We define WEIGHT and WIDTH first as arrays
+        and then as hashtables; this allows us to use the array-variants
+        as an *ordered* (by weight/width) list of values (in the routines
+        get_all_nfss_weights and get_all_nfss_widths).
+
+=end Comment
+
+=cut
+
+my @WEIGHT = (
+    ul  =>  [ qw( ultralight thin 100 hairline ) ],
+    el  =>  [ qw( extralight 200 ) ],
+    l   =>  [ qw( light 300 ) ],
+    sl  =>  [ qw( semilight blond ) ],
+    ''  =>  [ qw( regular normal text book 400 normallight normaldark ) ],
+    sb  =>  [ qw( semibold demibold 600 medium 500 ) ],
+    b   =>  [ qw( bold 700 ) ],
+    eb  =>  [ qw( extrabold 800 ) ],
+    ub  =>  [ qw( ultrabold black heavy extrablack ultrablack 900 fatface
+                  ultraheavy poster super 1000 ) ],
+);
+
+my @WIDTH = (
+    uc  =>  [ qw( ultracondensed extracompressed ultracompressed ) ],
+    ec  =>  [ qw( extracondensed compressed compact ) ],
+    c   =>  [ qw( condensed ) ],
+    sc  =>  [ qw( semicondensed narrow ) ],
+    ''  =>  [ qw( regular ) ],
+    sx  =>  [ qw( semiextended semiexpanded wide ) ],
+    x   =>  [ qw( extended expanded ) ],
+    ex  =>  [],
+    ux  =>  [],
+);
+
+my %WEIGHT = @WEIGHT;
+ at WEIGHT = grep { !ref } @WEIGHT;
+# Add abbreviated forms of weight names.
+for my $code (@WEIGHT) {
+    $WEIGHT{$code}
+        = [ map { _get_abbreviated_forms($_) } @{$WEIGHT{$code}} ];
+}
+
+my @allweights = grep { $_ !~ m/ medium | regular | text /xms }
+                      map { @{$_} } values %WEIGHT;
+ at allweights = (
+    Util::sort_desc_length(@allweights),
+    qw(medium regular text)
+);
+
+
+my %WIDTH = @WIDTH;
+ at WIDTH = grep { !ref } @WIDTH;
+# Add abbreviated forms.
+for my $code (@WIDTH) {
+    $WIDTH{$code}
+        = [ map { _get_abbreviated_forms($_) } @{$WIDTH{$code}} ];
+}
+
+my @allwidths = grep { $_ ne 'regular' } map { @{$_} } values %WIDTH;
+ at allwidths = Util::sort_desc_length(@allwidths);
+
+
+=begin Comment
+
+    The SHAPE table maps various shape names to NFSS codes.
+
+    Like in the other * tables, entries may be added to teach autoinst
+    about new shapes. Note that this table is "the other way around"
+    compared to WEIGHT and WIDTH; those map NFSS codes to names,
+    this one maps names to NFSS codes. That's because the data from
+    this table is used in a slightly different way; notably, it isn't
+    used in the map_nfss_codes routine.
+
+=end Comment
+
+=cut
+
+my %SHAPE = (
+    roman       =>  'n',
+    upright     =>  'n',
+    italic      =>  'it',
+    inclined    =>  'sl',
+    oblique     =>  'sl',
+    slanted     =>  'sl',
+    romani      =>  'n',    # Silentium has two roman shapes, but no italic;
+    romanii     =>  'it',   # so we cheat by mapping the second roman to 'it'
+);
+
+# Add abbreviated forms.
+for my $full (keys %abbrev) {
+    if (defined $SHAPE{$full}) {
+        for my $abbrev (_get_abbreviated_forms($full)) {
+            $SHAPE{$abbrev} = $SHAPE{$full};
+        }
+    }
+}
+
+my @allshapes = Util::sort_desc_length(keys %SHAPE);
+
+
 # --------------------------------------------------------------------------
-#   Collects all needed info about a font file.
+#   Returns the unabbreviated form of its argument,
+#   or its argument itself if no unabbreviated form is known.
 # --------------------------------------------------------------------------
-sub get_fontinfo {
-    my $filename = shift;
+sub unabbreviate {
+    my $key = shift;
 
-    my $info = Font::Info->new($filename);
+    return $FULL_FORM{$key} // $key;
+}
 
-    my $basicinfo = Font::Raw::get_basicinfo($filename);
-    $info->process_basicinfo($basicinfo);
 
-    my $os2_table = Font::Raw::get_classdata($filename);
-    $info->process_classdata($os2_table);
+# --------------------------------------------------------------------------
+#   Prepends weight names to the WEIGHT table for a given NFSS code.
+# --------------------------------------------------------------------------
+sub set_weight {
+    my ($key, @values) = @_;
 
-    my $featuredata = Font::Raw::get_featuredata($filename);
-    $info->process_featuredata($featuredata);
+    $WEIGHT{$key} = [ @values, @{$WEIGHT{$key}} ];
+    return;
+}
 
-    my $sizedata = Font::Raw::get_sizedata($filename);
-    $info->process_sizedata($sizedata);
 
-    my $nfssdata = Font::Raw::get_nfss_classification($filename);
-    $info->process_nfss_classification($nfssdata);
+# --------------------------------------------------------------------------
+#   Returns a list of NFSS weight names, sorted by weight.
+# --------------------------------------------------------------------------
+sub get_all_nfss_weights {
+    return @WEIGHT;
+}
 
-    return $info;
+
+# --------------------------------------------------------------------------
+#   Returns a list of all known weights, in an order suitable for searching.
+# --------------------------------------------------------------------------
+sub get_all_weights {
+    return @allweights;
 }
 
 
-# Error messages, used in assert_unique().
-my $ERR_DETAIL =<<'END_ERR_DETAIL';
-[ERROR]     I've parsed both %s
-                         and %s as
+# --------------------------------------------------------------------------
+#   Prepends weight names to the WIDTH table for a given NFSS code.
+# --------------------------------------------------------------------------
+sub set_width {
+    my ($key, @values) = @_;
 
-            Family:     %s
-            Weight:     %s
-            Width:      %s
-            Shape:      %s
-            Size:       %s-%s
-            Smallcaps:  %s
+    $WIDTH{$key} = [ @values, @{$WIDTH{$key}} ];
+    return;
+}
 
-END_ERR_DETAIL
 
-my $ERR_PARSE =<<'END_ERR_PARSE';
-[ERROR]     I failed to parse all fonts in a unique way;
-            presumably some fonts have unusual widths, weights or shapes.
+# --------------------------------------------------------------------------
+#   Returns a list of NFSS width names, sorted by width.
+# --------------------------------------------------------------------------
+sub get_all_nfss_widths {
+    return @WIDTH;
+}
 
-            Try one of the following:
-            -   Run 'autoinst' on a smaller set of fonts,
-                omitting the ones that weren't parsed correctly;
-            -   Add the missing widths, weights or shapes to the tables
-                'WIDTH', 'WEIGHT' or 'SHAPE' in the source code;
 
-            Please also send a bug report to the author.
-END_ERR_PARSE
+# --------------------------------------------------------------------------
+#   Returns a list of all known widths, in an order suitable for searching.
+# --------------------------------------------------------------------------
+sub get_all_widths {
+    return @allwidths;
+}
 
+
 # --------------------------------------------------------------------------
-#   Asserts all parsed fonts are unique.
+#   Returns the NFSS code for the given shape.
 # --------------------------------------------------------------------------
-sub assert_unique {
-    my ($log, $fontlist) = @_;
+sub to_nfss {
+    my $key = shift;
 
-    # These attributes should uniquely identify each font.
-    my @attributes
-        = qw(family weight width shape minsize maxsize is_smallcaps);
+    return $SHAPE{$key};
+}
 
-    my (%seen, $err_details);
+
+# --------------------------------------------------------------------------
+#   Returns a list of all known shapes, in an order suitable for searching.
+# --------------------------------------------------------------------------
+sub get_all_shapes {
+    return @allshapes;
+}
+
+
+# --------------------------------------------------------------------------
+#   Returns mappings of NFSS codes to weight and width, and vice versa.
+# --------------------------------------------------------------------------
+sub map_nfss_codes {
+    my $fontlist = shift;
+
+    my (%weight, %width);
     for my $font (@{$fontlist}) {
-        my $key = join "\x00", @{$font}{@attributes};
+        $weight{ $font->{weight} } //= $font->{weight_class};
+        $width{  $font->{width} }  //= $font->{width_class};
+    }
 
-        if ($seen{$key}) {
-            $err_details .= sprintf $ERR_DETAIL,
-                                    $seen{$key}{filename},
-                                    $font->{filename},
-                                    @{$font}{@attributes};
+    my $from_nfss = {
+        weight => {},
+        width => {},
+    };
+
+    for my $nfssweight (get_all_nfss_weights()) {
+        $from_nfss->{weight}{$nfssweight}
+            = [ grep { $weight{$_} } @{$WEIGHT{$nfssweight}} ];
+    }
+
+    # Some trickery to handle the case where the ul/ub codes are mapped
+    # but the el/eb codes are still empty. We try two things:
+    # 1.  if there is a Thin (Heavy) weight and this is less extreme
+    #     than the weight mapped to ul (ub), we map Thin (Heavy) to ul (ub)
+    # 2.  otherwise we move the ul/ub weight to the el/eb position,
+    #     unless that weight is the Ultralight/Ultrabold weight
+    if (!$ARGV{el} and !$ARGV{ul}) {
+        if (@{$from_nfss->{weight}{ul}}
+                and !@{$from_nfss->{weight}{el}}) {
+            if ($weight{thin}
+                    and $weight{thin}
+                        > $weight{$from_nfss->{weight}{ul}[0]}) {
+                $from_nfss->{weight}{el} = ['thin',];
+            }
+            elsif ($from_nfss->{weight}{ul}[0] ne 'ultralight') {
+                $from_nfss->{weight}{el}
+                    = [ shift @{$from_nfss->{weight}{ul}} ];
+            }
         }
-        else {
-             $seen{$key} = $font;
+    }
+    if (!$ARGV{eb} and !$ARGV{ub}) {
+        if (@{$from_nfss->{weight}{ub}}
+                and !@{$from_nfss->{weight}{eb}}) {
+            if ($weight{heavy}
+                    and $weight{heavy}
+                        < $weight{$from_nfss->{weight}{ub}[0]}) {
+                $from_nfss->{weight}{eb} = ['heavy',]
+                    unless @{$from_nfss->{weight}{b}}
+                       and $weight{$from_nfss->{weight}{b}[0]}
+                           > $weight{heavy};
+            }
+            elsif ($from_nfss->{weight}{ub}[0] ne 'ultrabold') {
+                $from_nfss->{weight}{eb}
+                    = [ shift @{$from_nfss->{weight}{ub}} ];
+            }
         }
     }
 
-    # Die with detailed error message if the font infos aren't unique.
-    if ($err_details) {
-        $log->log($err_details, $ERR_PARSE);
-        die $err_details, $ERR_PARSE;
+    # Special case: if we don't have Regular but we *do* have Medium,
+    # move Medium from the "sb" list to the "m" (i.e., Regular) one.
+    if (!@{$from_nfss->{weight}{''}}) {
+        my $alternate = ( grep { $weight{$_} } qw(medium 500) )[0];
+        if ($alternate) {
+            $from_nfss->{weight}{''} = [$alternate];
+            $from_nfss->{weight}{sb}
+                = [ grep { $_ ne $alternate } @{$from_nfss->{weight}{sb}} ];
+        }
     }
 
-    return 1;
+    # Some more trickery to map the sl code to Book or Text (but of course
+    # only if sl is empty and Book/Text is lighter than Regular)
+    if (!@{$from_nfss->{weight}{sl}}) {
+        $from_nfss->{weight}{sl}
+            = [ grep { $weight{$_} < $weight{$from_nfss->{weight}{''}[0]} }
+                     @{$from_nfss->{weight}{''}} ];
+    }
+
+    NFSSWIDTH:
+    for my $nfsswidth (get_all_nfss_widths()) {
+        for my $width ( @{$WIDTH{$nfsswidth}} ) {
+            if ($width{$width}) {
+                $from_nfss->{width}{$nfsswidth} = [$width];
+                next NFSSWIDTH;
+            }
+        }
+        $from_nfss->{width}{$nfsswidth} = [];
+    }
+
+    # Reverse the %from_nfss mapping to get %to_nfss
+    my %to_nfss;
+    NFSSWEIGHT:
+    for my $nfssweight (get_all_nfss_weights()) {
+        next unless @{$from_nfss->{weight}{$nfssweight}};
+        my $weight = $from_nfss->{weight}{$nfssweight}[0];
+        $weight = "" if $weight eq 'regular';
+
+        NFSSWIDTH:
+        for my $nfsswidth (get_all_nfss_widths()) {
+            my $nfssseries = ($nfssweight . $nfsswidth) || 'm';
+
+            next unless @{$from_nfss->{width}{$nfsswidth}};
+            my $width = $from_nfss->{width}{$nfsswidth}[0];
+            $width = "" if $width eq 'regular';
+            my $series = ($weight . $width) || 'regular';
+            $to_nfss{$series} = $nfssseries;
+        }
+    }
+
+    return [ $from_nfss, \%to_nfss ];
 }
 
 
@@ -248,13 +550,13 @@
 ############################################################################
 
 
-package Font::Info;
+package Font;
 
 # --------------------------------------------------------------------------
-#   Constructor: returns a new (mostly empty) Font::Info object.
+#   Constructor: returns a new Font object.
 # --------------------------------------------------------------------------
-sub new {
-    my ($cls, $filename) = @_;
+sub _new {
+    my ($class, $filename) = @_;
 
     my $self = {
         filename     => $filename,
@@ -266,24 +568,60 @@
         is_smallcaps => 0,
         weight_class => 0,
         width_class  => 0,
+        nfss         => 'rm',
     };
 
-    my ($ext) = $filename =~ m/[.] ([^.]+) \z/xmsi;
-    $ext = lc $ext;
-    if    ($ext eq 'otf') { $self->{fonttype} = 'opentype' }
-    elsif ($ext eq 'ttf') { $self->{fonttype} = 'truetype' }
-    else {
-        die "[ERROR]     Unknown font type '.$ext' ($filename)";
-    }
+    return bless $self, $class;
+}
 
-    return bless $self, $cls;
+
+# --------------------------------------------------------------------------
+#   Factory function: parses a font's metadata and creates a Font object.
+# --------------------------------------------------------------------------
+sub parse {
+    my $filename = shift;
+
+    my $self = __PACKAGE__->_new($filename);
+
+    my $metadata = _get_metadata($filename);
+
+    $self->_parse_metadata($metadata)
+         ->_parse_os2data()
+         ->_parse_featuredata()
+         ->_parse_sizedata()
+         ->_parse_nfss_classification()
+         ->_parse_fonttype()
+         ;
+
+    return $self;
 }
 
 
 # --------------------------------------------------------------------------
-#   Processes the basic info (given as a list of key-value pairs) for a font.
+#   Returns the output of otfinfo -i as a list of key-value pairs.
 # --------------------------------------------------------------------------
-sub process_basicinfo {
+sub _get_metadata {
+    my $filename = shift;
+
+    my $cmd = qq(otfinfo --info "$filename");
+    open my $otfinfo, '-|', $cmd
+        or die "[ERROR]     Could not fork(): $!";
+    my %data = map { my ($k,$v) = m/\A\s* ([^:]+?) \s*:\s* ([^\r\n]+)/xms;
+                     $k =~ s/\s+//xmsg;
+                     (lc $k => $v);
+                   }
+                   grep { m/\A\s* [^:]+? \s*:\s* [^\r\n]+/xms } <$otfinfo>;
+    close $otfinfo
+        or die "[ERROR]     '$cmd' failed.";
+
+    return \%data;
+}
+
+
+# --------------------------------------------------------------------------
+#   Processes the basic metadata for this font.
+# --------------------------------------------------------------------------
+sub _parse_metadata {
     my ($self, $data) = @_;
 
     $self->{originalfamily} = $data->{family};
@@ -303,6 +641,11 @@
     $data->{family}    =~ s/(\d)/$DIGITS[$1]/xmsge;
     $data->{family}    =~ s/[^A-Za-z]+//xmsg;
 
+    if ($data->{family} =~ m/\A DTL/xms) {
+        $data->{family}    =~ s/\A DTL//xms;
+        $data->{subfamily} =~ s/\A (?: ST | T)//xms;
+    }
+
     # remove Adobe's SmallText size, to avoid mistaking it for Text weight
     $data->{family}    =~ s/(?: SmallText | SmText )\z//xmsi;
     $data->{subfamily} =~ s/(?: SmallText | SmText )\z//xmsi;
@@ -322,7 +665,7 @@
     # 3.  Test the weights 'medium' and 'regular' *last*, since these strings
     #     may also occur in Subfamily without indicating the weight;
     #     so we only take them to mean weight if we find no other hit.
-    my @widths = NFSS::get_all_widths();
+    my @widths = Attr::get_all_widths();
     for my $width (@widths) {
         if ($fullinfo =~ m/$width/xms) {
             $self->{width} = $width;
@@ -331,7 +674,7 @@
             last;
         }
     }
-    my @weights = NFSS::get_all_weights();
+    my @weights = Attr::get_all_weights();
     for my $weight (@weights) {
         if ($fullinfo =~ m/$weight/xms) {
             $self->{weight} = $weight;
@@ -340,7 +683,7 @@
             last;
         }
     }
-    my @shapes = NFSS::get_all_shapes();
+    my @shapes = Attr::get_all_shapes();
     for my $shape (@shapes) {
         if ($fullinfo =~ m/$shape/xms) {
             $self->{shape} = $shape;
@@ -362,9 +705,9 @@
 
     # Take care to unabbreviate weight and width; CondensedUltra fonts
     # might end up as 'ultracondensed' instead of 'ultrablackcondensed'!
-    $self->{width}  = NFSS::unabbreviate($self->{width});
-    $self->{weight} = NFSS::unabbreviate($self->{weight});
-    $self->{shape}  = NFSS::unabbreviate($self->{shape});
+    $self->{width}  = Attr::unabbreviate($self->{width});
+    $self->{weight} = Attr::unabbreviate($self->{weight});
+    $self->{shape}  = Attr::unabbreviate($self->{shape});
 
     # Some font families put small caps into separate families;
     # we merge these into the 'main' family.
@@ -380,6 +723,9 @@
         $self->{subfamily}    = $1;
         $self->{is_smallcaps} = 1;
     }
+    if ($self->{name} =~ m/\A DTL/xms && $self->{subfamily} =~ s/Caps//xms) {
+        $self->{is_smallcaps} = 1;
+    }
     if ($self->{name} =~ m/(.+?) (?: $shapes) \z/xmsi) {
         $self->{is_smallcaps} = 1;
     }
@@ -388,19 +734,19 @@
     $shapes = join '|', Util::sort_desc_length(qw(it italic));
     if ($self->{family} =~ m/(.+?) ($shapes) \z/xmsi
             and ($self->{shape} eq 'regular'
-                    or $self->{shape} eq NFSS::unabbreviate(lc($2)))) {
+                    or $self->{shape} eq Attr::unabbreviate(lc($2)))) {
         $self->{family} = $1;
-        $self->{shape}  = NFSS::unabbreviate(lc($2));
+        $self->{shape}  = Attr::unabbreviate(lc($2));
     }
 
     # Some font families put different widths into separate families;
     # we merge these into the 'main' font family.
-    my $widths = join '|', NFSS::get_all_widths();
+    my $widths = join '|', Attr::get_all_widths();
     if ($self->{family} =~ m/(.+?) ($widths) \z/xmsi
             and ($self->{width} eq 'regular'
-                    or $self->{width} eq NFSS::unabbreviate(lc($2)))) {
+                    or $self->{width} eq Attr::unabbreviate(lc($2)))) {
         $self->{family} = $1;
-        $self->{width}  = NFSS::unabbreviate(lc($2));
+        $self->{width}  = Attr::unabbreviate(lc($2));
     }
 
     # Some font families put unusual weights into separate families;
@@ -407,18 +753,18 @@
     # we merge these into the 'main' font family. But we have to be
     # careful with the word 'Text': this might be part of the family name
     # (i.e., Libre Caslon Text) and should not be mistaken for a weight.
-    my $weights = join '|', NFSS::get_all_weights();
+    my $weights = join '|', Attr::get_all_weights();
     if ($self->{family} =~ m/text \z/xmsi) {
         $weights =~ s/[|]? text//xms;
     }
     if ($self->{family} =~ m/(.+?) ($weights) \z/xmsi
             and ($self->{weight} eq 'regular'
-                    or $self->{weight} eq NFSS::unabbreviate(lc($2)))) {
+                    or $self->{weight} eq Attr::unabbreviate(lc($2)))) {
         $self->{family} = $1;
-        $self->{weight} = NFSS::unabbreviate(lc($2));
+        $self->{weight} = Attr::unabbreviate(lc($2));
     }
 
-    $self->{basicshape} = NFSS::get_nfss_shape($self->{shape});
+    $self->{basicshape} = Attr::to_nfss($self->{shape});
 
     # We define 'series' as 'weight + width'. This matches NFSS,
     # but contradicts how most fonts are named (which is 'width + weight').
@@ -428,32 +774,64 @@
         :                                  $self->{weight} . $self->{width}
         ;
 
-    return;
+    return $self;
 }
 
 
 # --------------------------------------------------------------------------
-#   Processes the usWeightClass and usWidthClass data.
+#   Parses usWeightClass and usWidthClass from the OS/2 table.
 # --------------------------------------------------------------------------
-sub process_classdata {
-    my ($self, $classdata) = @_;
+sub _parse_os2data {
+    my $self = shift;
 
-    $self->{weight_class} = $classdata->{weight_class};
-    $self->{width_class}  = $classdata->{width_class};
+    my $os2_table;
+    eval {
+        my $cmd = qq(otfinfo --dump-table "OS/2" "$self->{filename}");
+        open my $otfinfo, '-|:raw', $cmd
+            or die "could not fork(): $!";
+        $os2_table = do { local $/; <$otfinfo> };
+        close $otfinfo
+            or die "'$cmd' failed";
+    } or warn "[WARNING]   $@";
 
-    return;
+    my ($weight_class, $width_class) = unpack '@4n @6n', $os2_table;
+
+    $self->{weight_class} = $weight_class;
+    $self->{width_class}  = $width_class;
+
+    return $self;
 }
 
 
 # --------------------------------------------------------------------------
-#   Processes the list of features this font supports.
+#   Returns a list of features that this font supports.
+#   We include 'kern' in this list even if there is only a 'kern' table
+#   (but no feature), since otftotfm can also use the (legacy) table.
 # --------------------------------------------------------------------------
-sub process_featuredata {
-    my ($self, $data) = @_;
+sub _parse_featuredata {
+    my $self = shift;
 
-    %{$self->{feature}} = map { $_ => 1 } @$data;
+    my $cmd = qq(otfinfo --features "$self->{filename}");
+    open my $otfinfo, '-|', $cmd
+        or die "[ERROR]     Could not fork(): $!";
+    my @data = map { substr $_, 0, 4 } <$otfinfo>;
+    close $otfinfo
+        or die "[ERROR]     '$cmd' failed.";
 
-    return;
+    $cmd = qq(otfinfo --tables "$self->{filename}");
+    open $otfinfo, '-|', $cmd
+        or die "[ERROR]     Could not fork(): $!";
+    my $data = do { local $/; <$otfinfo> };
+    close $otfinfo
+        or die "[ERROR]     '$cmd' failed.";
+
+    if ($data =~ m/\d+ \s+ kern/xms) {
+        push @data, 'kern';
+    }
+
+    %{$self->{feature}} = map { $_ => 1 } @data;
+
+    return $self;
 }
 
 
@@ -460,11 +838,23 @@
 # --------------------------------------------------------------------------
 #   Extracts 'minsize' and 'maxsize' from the optical design size info.
 # --------------------------------------------------------------------------
-sub process_sizedata {
-    my ($self, $sizedata) = @_;
+sub _parse_sizedata {
+    my $self = shift;
 
-    my ($minsize, $maxsize) = @$sizedata;
+    my $cmd = qq(otfinfo --optical-size "$self->{filename}");
+    open my $otfinfo, '-|', $cmd
+        or die "[ERROR]     Could not fork(): $!";
+    my $data = do { local $/; <$otfinfo> };
+    close $otfinfo
+        or die "[ERROR]     '$cmd' failed.";
 
+    my ($minsize, $maxsize)
+        = $data =~ m/[(] ([\d.]+) \s* pt, \s*
+                         ([\d.]+) \s* pt  \s* [])]/xms;
+
+    $minsize //= 0;
+    $maxsize //= 0;
+
     # fix some known bugs
     if ($self->{name} eq 'GaramondPremrPro-It'
         && $minsize == 6 && $maxsize == 8.9) {
@@ -488,7 +878,7 @@
 
     @{$self}{qw(minsize maxsize)} = ($minsize, $maxsize);
 
-    return;
+    return $self;
 }
 
 
@@ -495,146 +885,126 @@
 # --------------------------------------------------------------------------
 #   Adds the NFSS classification (rm, sf, tt) to self.
 # --------------------------------------------------------------------------
-sub process_nfss_classification {
-    my ($self, $data) = @_;
+sub _parse_nfss_classification {
+    my $self = shift;
 
-    $self->{nfss} = $data;
-
-    return;
-}
-
-
-############################################################################
-
-
-package Font::Raw;
-
-# --------------------------------------------------------------------------
-#   Returns the output of otfinfo -i as a list of key-value pairs.
-# --------------------------------------------------------------------------
-sub get_basicinfo {
-    my $filename = shift;
-
-    my $cmd = qq(otfinfo --info "$filename");
-    open my $otfinfo, '-|', $cmd
-        or die "[ERROR]     Could not fork(): $!";
-    my %data = map { my ($k,$v) = m/\A\s* ([^:]+?) \s*:\s* ([^\r\n]+)/xms;
-                     $k =~ s/\s+//xmsg;
-                     (lc $k => $v);
-                   }
-                   grep { m/\A\s* [^:]+? \s*:\s* [^\r\n]+/xms } <$otfinfo>;
-    close $otfinfo
-        or die "[ERROR]     '$cmd' failed.";
-
-    return \%data;
-}
-
-
-# --------------------------------------------------------------------------
-#   Returns usWeightClass and usWidthClass from the OS/2 table.
-# --------------------------------------------------------------------------
-sub get_classdata {
-    my $filename = shift;
-
-    my $os2_table;
+    my $classification;
     eval {
-        my $cmd = qq(otfinfo --dump-table "OS/2" "$filename");
+        my $cmd = qq(otfinfo --dump-table "post" "$self->{filename}");
         open my $otfinfo, '-|:raw', $cmd
             or die "could not fork(): $!";
-        $os2_table = do { local $/; <$otfinfo> };
+        my $post_table = do { local $/; <$otfinfo> };
         close $otfinfo
             or die "'$cmd' failed";
+
+        my $is_fixed_pitch = unpack '@12N', $post_table;
+
+        $self->{nfss} = $is_fixed_pitch                          ? 'tt'
+                      : $self->{filename} =~ m/mono(?!type)/xmsi ? 'tt'
+                      : $self->{filename} =~ m/sans/xmsi         ? 'sf'
+                      :                                            'rm'
+                      ;
     } or warn "[WARNING]   $@";
 
-    my ($weight_class, $width_class) = unpack '@4n @6n', $os2_table;
-
-    return {
-        weight_class => $weight_class,
-        width_class  => $width_class,
-    };
+    return $self;
 }
 
 
 # --------------------------------------------------------------------------
-#   Returns a list of features that this font supports.
-#   We include 'kern' in this list even if there is only a 'kern' table
-#   (but no feature), since otftotfm can also use the (legacy) table.
+#   Determines the OpenType flavor of this font.
 # --------------------------------------------------------------------------
-sub get_featuredata {
-    my $filename = shift;
+sub _parse_fonttype() {
+    my $self = shift;
 
-    my $cmd = qq(otfinfo --features "$filename");
-    open my $otfinfo, '-|', $cmd
-        or die "[ERROR]     Could not fork(): $!";
-    my @data = map { substr $_, 0, 4 } <$otfinfo>;
-    close $otfinfo
-        or die "[ERROR]     '$cmd' failed.";
+    my ($ext) = $self->{filename} =~ m/[.] ([^.]+) \z/xmsi;
+    $ext = lc $ext;
 
-    $cmd = qq(otfinfo --tables "$filename");
-    open $otfinfo, '-|', $cmd
-        or die "[ERROR]     Could not fork(): $!";
-    my $data = do { local $/; <$otfinfo> };
-    close $otfinfo
-        or die "[ERROR]     '$cmd' failed.";
+    $self->{fonttype}
+        = $ext eq 'otf' ? 'opentype'
+        : $ext eq 'ttf' ? 'truetype'
+        :                 ''
+        ;
 
-    if ($data =~ m/\d+ \s+ kern/xms) {
-        push @data, 'kern';
+    if (!$self->{fonttype}) {
+        open my $fontfile, '<:raw', $self->{filename}
+            or die "[ERROR]     Could not open '$self->{filename}': $!";
+        my $fontsignature;
+        read $fontfile, $fontsignature, 4;
+        $self->{fonttype}
+            = $fontsignature eq 'OTTO'             ? 'opentype'
+            : $fontsignature eq "\x00\x01\x00\x00" ? 'truetype'
+            :                                        ''
+            ;
+        close $fontfile;
     }
 
-    return \@data;
+    if (!$self->{fonttype}) {
+        die "[ERROR]     Unknown font type ($self->{filename})"
+    }
+
+    return $self;
 }
 
 
-# --------------------------------------------------------------------------
-#   Returns the size info for this font (which may be empty).
-# --------------------------------------------------------------------------
-sub get_sizedata {
-    my $filename = shift;
+# Error messages, used in assert_unique().
+my $ERR_DETAIL =<<'END_ERR_DETAIL';
+[ERROR]     I've parsed both %s
+                         and %s as
 
-    my $cmd = qq(otfinfo --optical-size "$filename");
-    open my $otfinfo, '-|', $cmd
-        or die "[ERROR]     Could not fork(): $!";
-    my $data = do { local $/; <$otfinfo> };
-    close $otfinfo
-        or die "[ERROR]     '$cmd' failed.";
+            Family:     %s
+            Weight:     %s
+            Width:      %s
+            Shape:      %s
+            Size:       %s-%s
+            Smallcaps:  %s
 
-    my ($minsize, $maxsize)
-        = $data =~ m/[(] ([\d.]+) \s* pt, \s*
-                         ([\d.]+) \s* pt  \s* [])]/xms;
+END_ERR_DETAIL
 
-    $minsize //= 0;
-    $maxsize //= 0;
-    return [ $minsize, $maxsize ];
-}
+my $ERR_PARSE =<<'END_ERR_PARSE';
+[ERROR]     I failed to parse all fonts in a unique way;
+            presumably some fonts have unusual widths, weights or shapes.
 
+            Try one of the following:
+            -   Run 'autoinst' on a smaller set of fonts,
+                omitting the ones that weren't parsed correctly;
+            -   Add the missing widths, weights or shapes to the tables
+                'WIDTH', 'WEIGHT' or 'SHAPE' in the source code;
 
+            Please also send a bug report to the author.
+END_ERR_PARSE
+
 # --------------------------------------------------------------------------
-#   Returns the NFSS classification (i.e., rm, sf or tt) for this font.
-#   Note that the algorithm used is "best effort only", so its results
-#   may be wrong.
+#   Asserts all parsed fonts are unique.
 # --------------------------------------------------------------------------
-sub get_nfss_classification {
-    my $filename = shift;
+sub assert_unique {
+    my ($log, $fontlist) = @_;
 
-    my $classification;
-    eval {
-        my $cmd = qq(otfinfo --dump-table "post" "$filename");
-        open my $otfinfo, '-|:raw', $cmd
-            or die "could not fork(): $!";
-        my $post_table = do { local $/; <$otfinfo> };
-        close $otfinfo
-            or die "'$cmd' failed";
+    # These attributes should uniquely identify each font.
+    my @attributes
+        = qw(family weight width shape minsize maxsize is_smallcaps);
 
-        my $is_fixed_pitch = unpack '@12N', $post_table;
+    my (%seen, $err_details);
+    for my $font (@{$fontlist}) {
+        my $key = join "\x00", @{$font}{@attributes};
 
-        $classification = $is_fixed_pitch                  ? 'tt'
-                        : $filename =~ m/mono(?!type)/xmsi ? 'tt'
-                        : $filename =~ m/sans/xmsi         ? 'sf'
-                        :                                    'rm'
-                        ;
-    } or warn "[WARNING]   $@";
+        if ($seen{$key}) {
+            $err_details .= sprintf $ERR_DETAIL,
+                                    $seen{$key}{filename},
+                                    $font->{filename},
+                                    @{$font}{@attributes};
+        }
+        else {
+             $seen{$key} = $font;
+        }
+    }
 
-    return $classification;
+    # Die with detailed error message if the font infos aren't unique.
+    if ($err_details) {
+        $log->log($err_details, $ERR_PARSE);
+        die $err_details, $ERR_PARSE;
+    }
+
+    return 1;
 }
 
 
@@ -1581,13 +1951,13 @@
     my ($self, $nfss_mapping) = @_;
 
     print {$self} "\n" . '-' x 76 . "\n\nNFSS mappings:\n\n";
-    for my $weight (NFSS::get_all_nfss_weights()) {
+    for my $weight (Attr::get_all_nfss_weights()) {
         printf {$self} "    %-3s =>  %s\n",
                        $weight || 'm',
                        $nfss_mapping->{weight}{$weight}[0] || '';
     }
     printf {$self} "\n";
-    for my $width (NFSS::get_all_nfss_widths()) {
+    for my $width (Attr::get_all_nfss_widths()) {
         printf {$self} "    %-3s =>  %s\n",
                        $width || 'm',
                        $nfss_mapping->{width}{$width}[0] || '';
@@ -1654,434 +2024,6 @@
 ############################################################################
 
 
-package NFSS;
-
-=begin Comment
-
-    Some fontnames contain abbreviated words for width, weight and/or shape;
-    we unabbreviate these using the following table.
-
-=end Comment
-
-=cut
-
-my %FULL_FORM = (
-    cmp     =>  'compressed',
-    comp    =>  'compressed',
-    cond    =>  'condensed',
-    demi    =>  'demibold',
-    extcond =>  'extracondensed',
-    hair    =>  'hairline',
-    incline =>  'inclined',
-    it      =>  'italic',
-    ita     =>  'italic',
-    md      =>  'medium',
-    slant   =>  'slanted',
-    ultra   =>  'ultrablack',
-);
-
-=begin Comment
-
-    LaTeX's NFSS contains a number of standard codes for weight and width:
-    - weight: ul, el, l, sl, m, sb, b, eb, ub
-    - width:  uc, ec, c, sc, m, sx, x, ex, ux
-
-    These codes are not always a perfect match with the weights and widths
-    present in a font family; some families (especially many sans serif ones)
-    contain more or different weights and widths, and the naming of those
-    weights and widths isn't always consistent between font families.
-    To handle this situation, we use a two-tiered approach:
-    1.  We install all fonts using a "series" name that is the concatenation
-        of whatever the font designer has chosen to call the weight and width
-        (but in all *lower*case).
-    2.  We add "alias" rules to the .fd files that map the standard NFSS codes
-        to actual fonts.
-
-    In step 1, we follow NFSS in leaving out any occurrence of
-    the word "regular" unless *both* weight and width are Regular;
-    in that case, the 'series' attribute becomes "regular".
-
-    The two tables WEIGHT and WIDTH are used to control step 2.
-    It contains several entries of the form
-
-        sc  =>  [ qw( semicondensed narrow ) ],
-
-    This should be read as follows: the NFSS code "sc" is mapped to
-    the *first* width on the right hand side present in the current family.
-
-    Please note that the tables contain empty keys instead of "m" for the
-    regular weight and width. NFSS actually combines weight and width into
-    a single "series" attribute; a weight or width of "m" is left out of
-    this combination (unless *both* weight and width are equal to "m"; then
-    the series becomes "m", but that's a special case we deal with later on).
-
-    In addition to the mapping of NFSS codes, the two mentioned tables are
-    also used in parsing the font's metadata to determine its weight and
-    width: any string that occurs on the right hand side is considered a
-    possible name to be searched for.
-
-    These tables can be extended to teach autoinst about new weights or
-    widths.  Suppose your font family contains a "Hemibold" weight, that
-    you want mapped to the "sb" code. Then add the name "hemibold" to
-    the right hand side of the "sb" entry in the WEIGHT table:
-
-        sb  =>  [ qw( semibold demibold medium hemibold ) ],
-
-    In this case, since it's in last position, it's only mapped to "sb"
-    if none of the other fonts are present. Put it earlier in the list
-    to give it higher priority.
-
-    Note that autoinst converts all metadata to lowercase to avoid
-    inconsistent capitalization; so all entries in these tables should
-    be *lowercase* as well.
-
-    Technical notes:
-    -   We define WEIGHT and WIDTH first as arrays
-        and then as hashtables; this allows us to use the array-variants
-        as an *ordered* (by weight/width) list of values (in the routines
-        get_all_nfss_weights and get_all_nfss_widths).
-
-=end Comment
-
-=cut
-
-my @WEIGHT = (
-    ul  =>  [ qw( ultralight thin 100 hairline ) ],
-    el  =>  [ qw( extralight 200 ) ],
-    l   =>  [ qw( light 300 ) ],
-    sl  =>  [ qw( semilight blond ) ],
-    ''  =>  [ qw( regular normal text book 400 ) ],
-    sb  =>  [ qw( semibold demibold 600 medium 500 ) ],
-    b   =>  [ qw( bold 700 ) ],
-    eb  =>  [ qw( extrabold 800 ) ],
-    ub  =>  [ qw( ultrabold black heavy extrablack ultrablack 900 fatface
-                  ultraheavy poster super 1000 ) ],
-);
-
-my @WIDTH = (
-    uc  =>  [ qw( ultracondensed extracompressed ultracompressed ) ],
-    ec  =>  [ qw( extracondensed compressed compact ) ],
-    c   =>  [ qw( condensed ) ],
-    sc  =>  [ qw( semicondensed narrow ) ],
-    ''  =>  [ qw( regular ) ],
-    sx  =>  [ qw( semiextended semiexpanded wide ) ],
-    x   =>  [ qw( extended expanded ) ],
-    ex  =>  [],
-    ux  =>  [],
-);
-
-=begin Comment
-
-    The SHAPE table maps various shape names to NFSS codes.
-
-    Like in the other * tables, entries may be added to teach autoinst
-    about new shapes. Note that this table is "the other way around"
-    compared to WEIGHT and WIDTH; those map NFSS codes to names,
-    this one maps names to NFSS codes. That's because the data from
-    this table is used in a slightly different way; notably, it isn't
-    used in the map_nfss_codes routine.
-
-=end Comment
-
-=cut
-
-my %SHAPE = (
-    roman       =>  'n',
-    upright     =>  'n',
-    italic      =>  'it',
-    inclined    =>  'sl',
-    oblique     =>  'sl',
-    slanted     =>  'sl',
-    romani      =>  'n',    # Silentium has two roman shapes, but no italic;
-    romanii     =>  'it',   # so we cheat by mapping the second roman to 'it'
-);
-
-
-####    FUNCTIONS FOR ACCESSING DATA FROM THE TABLES    ####
-
-
-# --------------------------------------------------------------------------
-#   Returns the unabbreviated form of its argument,
-#   or its argument itself if no unabbreviated form is known.
-# --------------------------------------------------------------------------
-sub unabbreviate {
-    my $key = shift;
-
-    return $FULL_FORM{$key} // $key;
-}
-
-
-# Auxiliary table that reverses FULL_FORM; maps full forms to abbrevs.
-my %abbrev;
-while (my ($k, $v) = each %FULL_FORM) {
-    push @{$abbrev{$v}}, $k;
-}
-for my $full (keys %abbrev) {
-    push @{$abbrev{$full}}, $full;
-}
-
-
-# --------------------------------------------------------------------------
-#   Returns a list of known abbreviations of its argument,
-#   or a singleton list containing just the argument itself
-#   if no abbreviations are known.
-# --------------------------------------------------------------------------
-sub get_abbreviated_forms {
-    my $key = shift;
-
-    return @{ $abbrev{$key} // [$key] };
-}
-
-
-my %WEIGHT = @WEIGHT;
- at WEIGHT = grep { !ref } @WEIGHT;
-
-# Add abbreviated forms, using the %ABBREV table constructed earlier.
-for my $code (@WEIGHT) {
-    $WEIGHT{$code}
-        = [ map { get_abbreviated_forms($_) } @{$WEIGHT{$code}} ];
-}
-
-
-# --------------------------------------------------------------------------
-#   Returns all weight names that might map to the given NFSS code.
-# --------------------------------------------------------------------------
-sub get_weights {
-    my $key = shift;
-
-    return @{$WEIGHT{$key}};
-}
-
-
-# --------------------------------------------------------------------------
-#   Adds weight names to the WEIGHT table.
-# --------------------------------------------------------------------------
-sub set_weights {
-    my ($key, @values) = @_;
-
-    $WEIGHT{$key} = [ @values, @{$WEIGHT{$key}} ];
-    return;
-}
-
-
-# --------------------------------------------------------------------------
-#   Returns a list of NFSS weights, sorted from light to heavy.
-# --------------------------------------------------------------------------
-sub get_all_nfss_weights {
-    return @WEIGHT;
-}
-
-
-my @allweights = grep { $_ !~ m/ medium | regular | text /xms }
-                      map { @{$_} } values %WEIGHT;
- at allweights = (Util::sort_desc_length(@allweights), qw(medium regular text));
-
-# --------------------------------------------------------------------------
-#   Returns a list of all known weight names,
-#   in an order that's suitable for search routines.
-# --------------------------------------------------------------------------
-sub get_all_weights {
-    return @allweights;
-}
-
-my %WIDTH = @WIDTH;
- at WIDTH = grep { !ref } @WIDTH;
-
-# Add abbreviated forms, using the %ABBREV table constructed earlier.
-for my $code (@WIDTH) {
-    $WIDTH{$code}
-        = [ map { get_abbreviated_forms($_) } @{$WIDTH{$code}} ];
-}
-
-
-# --------------------------------------------------------------------------
-#   Returns all width names that might map to the given NFSS code.
-# --------------------------------------------------------------------------
-sub get_widths {
-    my $key = shift;
-
-    return @{$WIDTH{$key}};
-}
-
-
-# --------------------------------------------------------------------------
-#   Adds width names to the WEIGHT table.
-# --------------------------------------------------------------------------
-sub set_widths {
-    my ($key, @values) = @_;
-
-    $WIDTH{$key} = [ @values, @{$WIDTH{$key}} ];
-    return;
-}
-
-
-# --------------------------------------------------------------------------
-#   Returns a list of NFSS widths, sorted from narrow to wide.
-# --------------------------------------------------------------------------
-sub get_all_nfss_widths {
-    return @WIDTH;
-}
-
-
-my @allwidths = grep { $_ ne 'regular' } map { @{$_} } values %WIDTH;
- at allwidths = Util::sort_desc_length(@allwidths);
-
-# --------------------------------------------------------------------------
-#   Returns a list of all known width names,
-#   in an order that's suitable for search routines.
-# --------------------------------------------------------------------------
-sub get_all_widths {
-    return @allwidths;
-}
-
-
-# Add abbreviated forms to %SHAPE table.
-for my $full (keys %abbrev) {
-    if (defined $SHAPE{$full}) {
-        for my $abbrev (get_abbreviated_forms($full)) {
-            $SHAPE{$abbrev} = $SHAPE{$full};
-        }
-    }
-}
-
-# --------------------------------------------------------------------------
-#   Returns the NFSS code for the given shape.
-# --------------------------------------------------------------------------
-sub get_nfss_shape {
-    my $key = shift;
-
-    return $SHAPE{$key};
-}
-
-
-my @allshapes = Util::sort_desc_length(keys %SHAPE);
-
-# --------------------------------------------------------------------------
-#   Returns a list of all known shape names,
-#   in an order that's suitable for search routines.
-# --------------------------------------------------------------------------
-sub get_all_shapes {
-    return @allshapes;
-}
-
-# --------------------------------------------------------------------------
-#   Returns a mapping of NFSS codes to weight and width names.
-# --------------------------------------------------------------------------
-sub map_nfss_codes {
-    my $fontlist = shift;
-
-    my (%weight, %width);
-    for my $font (@{$fontlist}) {
-        $weight{ $font->{weight} } //= $font->{weight_class};
-        $width{ $font->{width} }   //= $font->{width_class};
-    }
-
-    my $mapping = {
-        weight => {},
-        width => {},
-    };
-
-    for my $nfssweight (NFSS::get_all_nfss_weights()) {
-        $mapping->{weight}{$nfssweight}
-            = [ grep { $weight{$_} } NFSS::get_weights($nfssweight) ];
-    }
-
-    # Some trickery to handle the case where the ul/ub codes are mapped
-    # but the el/eb codes are still empty. We try two things:
-    # 1.  if there is a Thin (Heavy) weight and this is less extreme
-    #     than the weight mapped to ul (ub), we map Thin (Heavy) to ul (ub)
-    # 2.  otherwise we move the ul/ub weight to the el/eb position,
-    #     unless that weight is the Ultralight/Ultrabold weight
-    if (!$ARGV{el} and !$ARGV{ul}) {
-        if (@{$mapping->{weight}{ul}} and !@{$mapping->{weight}{el}}) {
-            if ($weight{thin}
-                    and $weight{thin} > $weight{$mapping->{weight}{ul}[0]}) {
-                $mapping->{weight}{el} = ['thin',];
-            }
-            elsif ($mapping->{weight}{ul}[0] ne 'ultralight') {
-                $mapping->{weight}{el} = [ shift @{$mapping->{weight}{ul}} ];
-            }
-        }
-    }
-    if (!$ARGV{eb} and !$ARGV{ub}) {
-        if (@{$mapping->{weight}{ub}} and !@{$mapping->{weight}{eb}}) {
-            if ($weight{heavy}
-                    and $weight{heavy} < $weight{$mapping->{weight}{ub}[0]}) {
-                $mapping->{weight}{eb} = ['heavy',]
-                    unless @{$mapping->{weight}{b}}
-                       and $weight{$mapping->{weight}{b}[0]} > $weight{heavy};
-            }
-            elsif ($mapping->{weight}{ub}[0] ne 'ultrabold') {
-                $mapping->{weight}{eb} = [ shift @{$mapping->{weight}{ub}} ];
-            }
-        }
-    }
-
-    # Special case: if we don't have Regular but we *do* have Medium,
-    # move Medium from the "sb" list to the "m" (i.e., Regular) one.
-    if (!@{$mapping->{weight}{''}}) {
-        my $alternate = ( grep { $weight{$_} } qw(medium 500) )[0];
-        if ($alternate) {
-            $mapping->{weight}{''} = [$alternate];
-            $mapping->{weight}{sb}
-                = [ grep { $_ ne $alternate } @{$mapping->{weight}{sb}} ];
-        }
-    }
-
-    # Some more trickery to map the sl code to Book or Text (but of course
-    # only if sl is empty and Book/Text is lighter than Regular)
-    if (!@{$mapping->{weight}{sl}}) {
-        $mapping->{weight}{sl}
-            = [ grep { $weight{$_} < $weight{$mapping->{weight}{''}[0]} }
-                     @{$mapping->{weight}{''}} ];
-    }
-
-    NFSSWIDTH:
-    for my $nfsswidth (NFSS::get_all_nfss_widths()) {
-        for my $width ( NFSS::get_widths($nfsswidth) ) {
-            if ($width{$width}) {
-                $mapping->{width}{$nfsswidth} = [$width];
-                next NFSSWIDTH;
-            }
-        }
-        $mapping->{width}{$nfsswidth} = [];
-    }
-
-    return $mapping;
-}
-
-# --------------------------------------------------------------------------
-#   Returns a mapping of weight and width names to NFSS codes.
-# --------------------------------------------------------------------------
-sub invert_mapping {
-    my $nfss_mapping = shift;
-
-    my %to_nfss;
-    NFSSWEIGHT:
-    for my $nfssweight (NFSS::get_all_nfss_weights()) {
-        next unless @{$nfss_mapping->{weight}{$nfssweight}};
-        my $weight = $nfss_mapping->{weight}{$nfssweight}[0];
-        $weight = "" if $weight eq 'regular';
-
-        NFSSWIDTH:
-        for my $nfsswidth (NFSS::get_all_nfss_widths()) {
-            my $nfssseries = ($nfssweight . $nfsswidth) || 'm';
-
-            next unless @{$nfss_mapping->{width}{$nfsswidth}};
-            my $width = $nfss_mapping->{width}{$nfsswidth}[0];
-            $width = "" if $width eq 'regular';
-            my $series = ($weight . $width) || 'regular';
-            $to_nfss{$series} = $nfssseries;
-        }
-    }
-
-    return \%to_nfss;
-}
-
-
-############################################################################
-
-
 package Options;
 
 my $USAGE =<<'END_USAGE';
@@ -2228,7 +2170,7 @@
                                           "$key=$values";
                                      my @values = split m/,/, lc $values;
                                      $key = q{} if $key eq 'm';
-                                     NFSS::set_weights($key, @values);
+                                     Attr::set_weight($key, @values);
                                      $ARGV{$key} = 'user-defined';
                                  },
         'nfsswidth=s%'        => sub {
@@ -2238,7 +2180,7 @@
                                      push @{$ARGV{nfsswidth}},
                                           "$key=$values";
                                      $key = q{} if $key eq 'm';
-                                     NFSS::set_widths($key, @values);
+                                     Attr::set_width($key, @values);
                                      $ARGV{$key} = 'user-defined';
                                  },
         'math!'               => \$ARGV{math},
@@ -2438,7 +2380,7 @@
         code  => { n => 'sc', it => 'scit', sl => 'scsl' },
         reqd  => [ 'smcp' ],
         nice  => [],
-        extra => '--unicoding="germandbls =: SSsmall"',
+        extra => '--unicoding="germandbls =: SSsmall" --unicoding="ff =: FFsmall" --unicoding="fi =: FIsmall" --unicoding="ffi =: FFIsmall" --unicoding="fl =: FLsmall" --unicoding="ffl =: FFLsmall"',
         name  => 'sc',
     },
     swash => {
@@ -2738,7 +2680,7 @@
     my @encodings = split /,/, $ARGV{encoding};
     for my $enc (@encodings) {
         if ($enc !~ m/\A(OT1|T1|TS1|LY1|LGR|T2[ABC]|T3|TS3)\z/xmsi) {
-            my $try = Cwd::abs_path($enc);
+            my $try = $enc;
             $try .= '.enc' if $try !~ m/[.]enc\z/xms;
             if (!-e $try) {
                 die "[ERROR]     No .enc file found for '$enc'";
@@ -2753,7 +2695,7 @@
     # - the text encodings contain T1 and the user didn't turn off TS1
     if ($ARGV{textcomp} == 1
             or ($ARGV{textcomp} >= 1
-                and grep { $_ =~ m/T1/xmsi } @{$ARGV{encoding}})) {
+                and grep { uc $_ eq 'T1' } @{$ARGV{encoding}})) {
         $ARGV{textcomp} = 1;
     }
     else {
@@ -3106,8 +3048,10 @@
         return;
     }
 
-    # Figure out which encoding file to use for this font.
-    my $try = Cwd::abs_path($workitem->{encoding});
+    # Figure out whether the encoding option refers to a custom encoding file
+    # in the current directory, or to the name of one of our standard
+    # encodings.  In the latter case, we add the 'fontools_' prefix.
+    my $try = $workitem->{encoding};
     $try .= '.enc' if $try !~ m/[.]enc\z/xms;
     if (-e $try) {
         $workitem->{enc_file} = $try;
@@ -3171,7 +3115,7 @@
         $ARGV{typeface} || $family);
 
     # The otftotfm-generated map file refers to the original otf files;
-    # we need to change this to use our name-modifyd Type1 fonts instead.
+    # we need to change this to use our name-modified Type1 fonts instead.
     my $mapfile = File::Spec->catfile(
         $ARGV{target},
         'fonts',
@@ -4053,7 +3997,7 @@
 
 =head1 VERSION
 
-This document describes B<autoinst> version 20200729.
+This document describes B<autoinst> version 20201218.
 
 
 =head1 RECENT CHANGES
@@ -4062,6 +4006,14 @@
 
 =over 12
 
+=item I<2020-12-18>
+
+Fixed a problem with files not being found on Windows.
+Added extra C<--unicoding> options to prevent getting
+lowercase f-ligatures in smallcaps for some buggy fonts.
+Optimized font info parsing for DTL and TypeBy font families.
+Cleaned up the code for better maintainability.
+
 =item I<2020-07-29>
 
 Some changes in the generated F<sty> and F<fd> files,

Modified: trunk/Build/source/texk/texlive/linked_scripts/fontools/ot2kpx
===================================================================
--- trunk/Build/source/texk/texlive/linked_scripts/fontools/ot2kpx	2020-12-19 21:25:38 UTC (rev 57170)
+++ trunk/Build/source/texk/texlive/linked_scripts/fontools/ot2kpx	2020-12-19 21:26:11 UTC (rev 57171)
@@ -38,7 +38,7 @@
 use List::Util @List::Util::EXPORT_OK;
 use Pod::Usage;
 
-my $VERSION = "20200729";
+my $VERSION = "20201218";
 
 our ($NUM_GLYPHS, $UNITS_PER_EM, %kern);
 
@@ -858,7 +858,7 @@
 
 =head1 VERSION
 
-This document describes B<ot2kpx> version 20200729.
+This document describes B<ot2kpx> version 20201218.
 
 
 =head1 RECENT CHANGES

Modified: trunk/Master/texmf-dist/doc/man/man1/afm2afm.1
===================================================================
--- trunk/Master/texmf-dist/doc/man/man1/afm2afm.1	2020-12-19 21:25:38 UTC (rev 57170)
+++ trunk/Master/texmf-dist/doc/man/man1/afm2afm.1	2020-12-19 21:26:11 UTC (rev 57171)
@@ -133,7 +133,7 @@
 .\" ========================================================================
 .\"
 .IX Title "AFM2AFM 1"
-.TH AFM2AFM 1 "2020-07-29" "fontools" "Marc Penninga"
+.TH AFM2AFM 1 "2020-12-18" "fontools" "Marc Penninga"
 .\" For nroff, turn off justification.  Always turn off hyphenation; it makes
 .\" way too many mistakes in technical documents.
 .if n .ad l
@@ -223,7 +223,7 @@
 See the \s-1GNU\s0 General Public License for more details.
 .SH "VERSION"
 .IX Header "VERSION"
-This document describes \fBafm2afm\fR version 20200729.
+This document describes \fBafm2afm\fR version 20201218.
 .SH "RECENT CHANGES"
 .IX Header "RECENT CHANGES"
 (See the source code for the rest of the story.)

Modified: trunk/Master/texmf-dist/doc/man/man1/afm2afm.man1.pdf
===================================================================
(Binary files differ)

Modified: trunk/Master/texmf-dist/doc/man/man1/autoinst.1
===================================================================
--- trunk/Master/texmf-dist/doc/man/man1/autoinst.1	2020-12-19 21:25:38 UTC (rev 57170)
+++ trunk/Master/texmf-dist/doc/man/man1/autoinst.1	2020-12-19 21:26:11 UTC (rev 57171)
@@ -133,7 +133,7 @@
 .\" ========================================================================
 .\"
 .IX Title "AUTOINST 1"
-.TH AUTOINST 1 "2020-07-29" "fontools" "Marc Penninga"
+.TH AUTOINST 1 "2020-12-18" "fontools" "Marc Penninga"
 .\" For nroff, turn off justification.  Always turn off hyphenation; it makes
 .\" way too many mistakes in technical documents.
 .if n .ad l
@@ -785,10 +785,17 @@
 \&\s-1GNU\s0 General Public License for more details.
 .SH "VERSION"
 .IX Header "VERSION"
-This document describes \fBautoinst\fR version 20200729.
+This document describes \fBautoinst\fR version 20201218.
 .SH "RECENT CHANGES"
 .IX Header "RECENT CHANGES"
 (See the source for the full story, all the way back to 2005.)
+.IP "\fI2020\-12\-18\fR" 12
+.IX Item "2020-12-18"
+Fixed a problem with files not being found on Windows.
+Added extra \f(CW\*(C`\-\-unicoding\*(C'\fR options to prevent getting
+lowercase f\-ligatures in smallcaps for some buggy fonts.
+Optimized font info parsing for \s-1DTL\s0 and TypeBy font families.
+Cleaned up the code for better maintainability.
 .IP "\fI2020\-07\-29\fR" 12
 .IX Item "2020-07-29"
 Some changes in the generated \fIsty\fR and \fIfd\fR files,

Modified: trunk/Master/texmf-dist/doc/man/man1/autoinst.man1.pdf
===================================================================
(Binary files differ)

Modified: trunk/Master/texmf-dist/doc/man/man1/ot2kpx.1
===================================================================
--- trunk/Master/texmf-dist/doc/man/man1/ot2kpx.1	2020-12-19 21:25:38 UTC (rev 57170)
+++ trunk/Master/texmf-dist/doc/man/man1/ot2kpx.1	2020-12-19 21:26:11 UTC (rev 57171)
@@ -133,7 +133,7 @@
 .\" ========================================================================
 .\"
 .IX Title "OT2KPX 1"
-.TH OT2KPX 1 "2020-07-29" "fontools" "Marc Penninga"
+.TH OT2KPX 1 "2020-12-18" "fontools" "Marc Penninga"
 .\" For nroff, turn off justification.  Always turn off hyphenation; it makes
 .\" way too many mistakes in technical documents.
 .if n .ad l
@@ -228,7 +228,7 @@
 See the \s-1GNU\s0 General Public License for more details.
 .SH "VERSION"
 .IX Header "VERSION"
-This document describes \fBot2kpx\fR version 20200729.
+This document describes \fBot2kpx\fR version 20201218.
 .SH "RECENT CHANGES"
 .IX Header "RECENT CHANGES"
 (See the source code for the rest of the story.)

Modified: trunk/Master/texmf-dist/doc/man/man1/ot2kpx.man1.pdf
===================================================================
(Binary files differ)

Modified: trunk/Master/texmf-dist/doc/support/fontools/splitttc
===================================================================
--- trunk/Master/texmf-dist/doc/support/fontools/splitttc	2020-12-19 21:25:38 UTC (rev 57170)
+++ trunk/Master/texmf-dist/doc/support/fontools/splitttc	2020-12-19 21:26:11 UTC (rev 57171)
@@ -38,7 +38,7 @@
 use Getopt::Long;
 use Pod::Usage;
 
-my $VERSION = "20200729";
+my $VERSION = "20201218";
 
 parse_commandline();
 
@@ -303,7 +303,7 @@
 
 =head1 VERSION
 
-This document describes B<splitttc> version 20200729.
+This document describes B<splitttc> version 20201218.
 
 
 =head1 RECENT CHANGES

Modified: trunk/Master/texmf-dist/scripts/fontools/afm2afm
===================================================================
--- trunk/Master/texmf-dist/scripts/fontools/afm2afm	2020-12-19 21:25:38 UTC (rev 57170)
+++ trunk/Master/texmf-dist/scripts/fontools/afm2afm	2020-12-19 21:26:11 UTC (rev 57171)
@@ -37,7 +37,7 @@
 use Getopt::Long;
 use Pod::Usage;
 
-my $VERSION = "20200729";
+my $VERSION = "20201218";
 
 parse_commandline();
 
@@ -421,7 +421,7 @@
 
 =head1 VERSION
 
-This document describes B<afm2afm> version 20200729.
+This document describes B<afm2afm> version 20201218.
 
 
 =head1 RECENT CHANGES

Modified: trunk/Master/texmf-dist/scripts/fontools/autoinst
===================================================================
--- trunk/Master/texmf-dist/scripts/fontools/autoinst	2020-12-19 21:25:38 UTC (rev 57170)
+++ trunk/Master/texmf-dist/scripts/fontools/autoinst	2020-12-19 21:26:11 UTC (rev 57171)
@@ -40,7 +40,7 @@
 use Pod::Usage ();
 use POSIX ();
 
-my $VERSION = '20200729';
+my $VERSION = '20201218';
 
 my ($d, $m, $y) = (localtime time)[3 .. 5];
 my $TODAY = sprintf "%04d/%02d/%02d", $y + 1900, $m + 1, $d;
@@ -63,10 +63,10 @@
 
         Log         Logs the font parsing and creation process.
 
-        Font        Code for getting font info. Contains two subpackages:
+        Attr        Contains tables and routines that represent
+                    font attributes (weight, width, shape).
 
-                    Font::Raw       Gets 'raw' data from font files.
-                    Font::Info      Extracts font info from raw data.
+        Font        Parses an OpenType font's metadata.
 
         Tables      Contains tables that drive the font creation process
                     (especially the decisions which fonts to create).
@@ -73,11 +73,6 @@
 
         Work        Generates all font files, driven by data from `Tables`.
 
-        NFSS        Contains tables and routines that map
-                    font characteristics (weight, width, shape)
-                    to NFSS attributes. This data is also used by
-                    `Font::Info`, to avoid duplication.
-
         LaTeX       Creates LaTeX support (.sty and .fd files).
 
         Otftotfm    Drives `otftotfm` to actually generate the fonts.
@@ -99,7 +94,7 @@
 
     my %fontfamily;
     for my $fontfile (@ARGV) {
-        my $font = Font::get_fontinfo($fontfile);
+        my $font = Font::parse($fontfile);
         my $family = $font->{family};
         push @{$fontfamily{$family}}, $font;
     }
@@ -117,8 +112,9 @@
         # until after the results of the parsing have been logged.
         Font::assert_unique($log, $fontlist);
 
-        my $nfss_mapping = NFSS::map_nfss_codes($fontlist);
-        $log->log_nfss_mapping($nfss_mapping);
+        my $mappings = Attr::map_nfss_codes($fontlist);
+        my ($from_nfss, $to_nfss) = @{$mappings};
+        $log->log_nfss_mapping($from_nfss);
 
         my @workitems = Work::generate_worklist($fontlist);
         $log->log_worklist(\@workitems);
@@ -129,8 +125,7 @@
         $log->log_commands(\@commands) if $ARGV{verbose} >= 1;
 
         if (!$ARGV{dryrun}) {
-            $nfss_mapping = NFSS::invert_mapping($nfss_mapping);
-            LaTeX::create_support_files(\@workitems, $family, $nfss_mapping);
+            LaTeX::create_support_files(\@workitems, $family, $to_nfss);
             Otftotfm::run_commands(\@commands, $family, $log);
 
             if ($ARGV{t1suffix}) {
@@ -154,94 +149,401 @@
 ############################################################################
 
 
-package Font;
+package Attr;
 
+=begin Comment
+
+    Some fontnames contain abbreviated words for width, weight and/or shape;
+    we unabbreviate these using the following table.
+
+=end Comment
+
+=cut
+
+my %FULL_FORM = (
+    cmp     =>  'compressed',
+    comp    =>  'compressed',
+    cond    =>  'condensed',
+    demi    =>  'demibold',
+    extcond =>  'extracondensed',
+    hair    =>  'hairline',
+    incline =>  'inclined',
+    it      =>  'italic',
+    ita     =>  'italic',
+    md      =>  'medium',
+    slant   =>  'slanted',
+    ultra   =>  'ultrablack',
+);
+
+# Auxiliary table that reverses FULL_FORM; maps full forms to abbrevs.
+my %abbrev;
+while (my ($k, $v) = each %FULL_FORM) {
+    push @{$abbrev{$v}}, $k;
+}
+
+# Add full forms as known abbreviations of themselves.
+for my $full (keys %abbrev) {
+    push @{$abbrev{$full}}, $full;
+}
+
+# Internal helper: returns a list of known abbreviations of its argument.
+sub _get_abbreviated_forms {
+    my $key = shift;
+
+    return @{ $abbrev{$key} // [$key] };
+}
+
+
+=begin Comment
+
+    LaTeX's NFSS contains a number of standard codes for weight and width:
+    - weight: ul, el, l, sl, m, sb, b, eb, ub
+    - width:  uc, ec, c, sc, m, sx, x, ex, ux
+
+    These codes are not always a perfect match with the weights and widths
+    present in a font family; some families (especially many sans serif ones)
+    contain more or different weights and widths, and the naming of those
+    weights and widths isn't always consistent between font families.
+    To handle this situation, we use a two-tiered approach:
+    1.  We install all fonts using a "series" name that is the concatenation
+        of whatever the font designer has chosen to call the weight and width
+        (but in all *lower*case).
+    2.  We add "alias" rules to the .fd files that map the standard NFSS codes
+        to actual fonts.
+
+    In step 1, we follow NFSS in leaving out any occurrence of
+    the word "regular" unless *both* weight and width are Regular;
+    in that case, the 'series' attribute becomes "regular".
+
+    The two tables WEIGHT and WIDTH are used to control step 2.
+    It contains several entries of the form
+
+        sc  =>  [ qw( semicondensed narrow ) ],
+
+    This should be read as follows: the NFSS code "sc" is mapped to
+    the *first* width on the right hand side present in the current family.
+
+    Please note that the tables contain empty keys instead of "m" for the
+    regular weight and width. NFSS actually combines weight and width into
+    a single "series" attribute; a weight or width of "m" is left out of
+    this combination (unless *both* weight and width are equal to "m"; then
+    the series becomes "m", but that's a special case we deal with later on).
+
+    In addition to the mapping of NFSS codes, the two mentioned tables are
+    also used in parsing the font's metadata to determine its weight and
+    width: any string that occurs on the right hand side is considered a
+    possible name to be searched for.
+
+    These tables can be extended to teach autoinst about new weights or
+    widths.  Suppose your font family contains a "Hemibold" weight, that
+    you want mapped to the "sb" code. Then add the name "hemibold" to
+    the right hand side of the "sb" entry in the WEIGHT table:
+
+        sb  =>  [ qw( semibold demibold medium hemibold ) ],
+
+    In this case, since it's in last position, it's only mapped to "sb"
+    if none of the other fonts are present. Put it earlier in the list
+    to give it higher priority.
+
+    Note that autoinst converts all metadata to lowercase to avoid
+    inconsistent capitalization; so all entries in these tables should
+    be *lowercase* as well.
+
+    Technical notes:
+    -   We define WEIGHT and WIDTH first as arrays
+        and then as hashtables; this allows us to use the array-variants
+        as an *ordered* (by weight/width) list of values (in the routines
+        get_all_nfss_weights and get_all_nfss_widths).
+
+=end Comment
+
+=cut
+
+my @WEIGHT = (
+    ul  =>  [ qw( ultralight thin 100 hairline ) ],
+    el  =>  [ qw( extralight 200 ) ],
+    l   =>  [ qw( light 300 ) ],
+    sl  =>  [ qw( semilight blond ) ],
+    ''  =>  [ qw( regular normal text book 400 normallight normaldark ) ],
+    sb  =>  [ qw( semibold demibold 600 medium 500 ) ],
+    b   =>  [ qw( bold 700 ) ],
+    eb  =>  [ qw( extrabold 800 ) ],
+    ub  =>  [ qw( ultrabold black heavy extrablack ultrablack 900 fatface
+                  ultraheavy poster super 1000 ) ],
+);
+
+my @WIDTH = (
+    uc  =>  [ qw( ultracondensed extracompressed ultracompressed ) ],
+    ec  =>  [ qw( extracondensed compressed compact ) ],
+    c   =>  [ qw( condensed ) ],
+    sc  =>  [ qw( semicondensed narrow ) ],
+    ''  =>  [ qw( regular ) ],
+    sx  =>  [ qw( semiextended semiexpanded wide ) ],
+    x   =>  [ qw( extended expanded ) ],
+    ex  =>  [],
+    ux  =>  [],
+);
+
+my %WEIGHT = @WEIGHT;
+ at WEIGHT = grep { !ref } @WEIGHT;
+# Add abbreviated forms of weight names.
+for my $code (@WEIGHT) {
+    $WEIGHT{$code}
+        = [ map { _get_abbreviated_forms($_) } @{$WEIGHT{$code}} ];
+}
+
+my @allweights = grep { $_ !~ m/ medium | regular | text /xms }
+                      map { @{$_} } values %WEIGHT;
+ at allweights = (
+    Util::sort_desc_length(@allweights),
+    qw(medium regular text)
+);
+
+
+my %WIDTH = @WIDTH;
+ at WIDTH = grep { !ref } @WIDTH;
+# Add abbreviated forms.
+for my $code (@WIDTH) {
+    $WIDTH{$code}
+        = [ map { _get_abbreviated_forms($_) } @{$WIDTH{$code}} ];
+}
+
+my @allwidths = grep { $_ ne 'regular' } map { @{$_} } values %WIDTH;
+ at allwidths = Util::sort_desc_length(@allwidths);
+
+
+=begin Comment
+
+    The SHAPE table maps various shape names to NFSS codes.
+
+    Like in the other * tables, entries may be added to teach autoinst
+    about new shapes. Note that this table is "the other way around"
+    compared to WEIGHT and WIDTH; those map NFSS codes to names,
+    this one maps names to NFSS codes. That's because the data from
+    this table is used in a slightly different way; notably, it isn't
+    used in the map_nfss_codes routine.
+
+=end Comment
+
+=cut
+
+my %SHAPE = (
+    roman       =>  'n',
+    upright     =>  'n',
+    italic      =>  'it',
+    inclined    =>  'sl',
+    oblique     =>  'sl',
+    slanted     =>  'sl',
+    romani      =>  'n',    # Silentium has two roman shapes, but no italic;
+    romanii     =>  'it',   # so we cheat by mapping the second roman to 'it'
+);
+
+# Add abbreviated forms.
+for my $full (keys %abbrev) {
+    if (defined $SHAPE{$full}) {
+        for my $abbrev (_get_abbreviated_forms($full)) {
+            $SHAPE{$abbrev} = $SHAPE{$full};
+        }
+    }
+}
+
+my @allshapes = Util::sort_desc_length(keys %SHAPE);
+
+
 # --------------------------------------------------------------------------
-#   Collects all needed info about a font file.
+#   Returns the unabbreviated form of its argument,
+#   or its argument itself if no unabbreviated form is known.
 # --------------------------------------------------------------------------
-sub get_fontinfo {
-    my $filename = shift;
+sub unabbreviate {
+    my $key = shift;
 
-    my $info = Font::Info->new($filename);
+    return $FULL_FORM{$key} // $key;
+}
 
-    my $basicinfo = Font::Raw::get_basicinfo($filename);
-    $info->process_basicinfo($basicinfo);
 
-    my $os2_table = Font::Raw::get_classdata($filename);
-    $info->process_classdata($os2_table);
+# --------------------------------------------------------------------------
+#   Prepends weight names to the WEIGHT table for a given NFSS code.
+# --------------------------------------------------------------------------
+sub set_weight {
+    my ($key, @values) = @_;
 
-    my $featuredata = Font::Raw::get_featuredata($filename);
-    $info->process_featuredata($featuredata);
+    $WEIGHT{$key} = [ @values, @{$WEIGHT{$key}} ];
+    return;
+}
 
-    my $sizedata = Font::Raw::get_sizedata($filename);
-    $info->process_sizedata($sizedata);
 
-    my $nfssdata = Font::Raw::get_nfss_classification($filename);
-    $info->process_nfss_classification($nfssdata);
+# --------------------------------------------------------------------------
+#   Returns a list of NFSS weight names, sorted by weight.
+# --------------------------------------------------------------------------
+sub get_all_nfss_weights {
+    return @WEIGHT;
+}
 
-    return $info;
+
+# --------------------------------------------------------------------------
+#   Returns a list of all known weights, in an order suitable for searching.
+# --------------------------------------------------------------------------
+sub get_all_weights {
+    return @allweights;
 }
 
 
-# Error messages, used in assert_unique().
-my $ERR_DETAIL =<<'END_ERR_DETAIL';
-[ERROR]     I've parsed both %s
-                         and %s as
+# --------------------------------------------------------------------------
+#   Prepends weight names to the WIDTH table for a given NFSS code.
+# --------------------------------------------------------------------------
+sub set_width {
+    my ($key, @values) = @_;
 
-            Family:     %s
-            Weight:     %s
-            Width:      %s
-            Shape:      %s
-            Size:       %s-%s
-            Smallcaps:  %s
+    $WIDTH{$key} = [ @values, @{$WIDTH{$key}} ];
+    return;
+}
 
-END_ERR_DETAIL
 
-my $ERR_PARSE =<<'END_ERR_PARSE';
-[ERROR]     I failed to parse all fonts in a unique way;
-            presumably some fonts have unusual widths, weights or shapes.
+# --------------------------------------------------------------------------
+#   Returns a list of NFSS width names, sorted by width.
+# --------------------------------------------------------------------------
+sub get_all_nfss_widths {
+    return @WIDTH;
+}
 
-            Try one of the following:
-            -   Run 'autoinst' on a smaller set of fonts,
-                omitting the ones that weren't parsed correctly;
-            -   Add the missing widths, weights or shapes to the tables
-                'WIDTH', 'WEIGHT' or 'SHAPE' in the source code;
 
-            Please also send a bug report to the author.
-END_ERR_PARSE
+# --------------------------------------------------------------------------
+#   Returns a list of all known widths, in an order suitable for searching.
+# --------------------------------------------------------------------------
+sub get_all_widths {
+    return @allwidths;
+}
 
+
 # --------------------------------------------------------------------------
-#   Asserts all parsed fonts are unique.
+#   Returns the NFSS code for the given shape.
 # --------------------------------------------------------------------------
-sub assert_unique {
-    my ($log, $fontlist) = @_;
+sub to_nfss {
+    my $key = shift;
 
-    # These attributes should uniquely identify each font.
-    my @attributes
-        = qw(family weight width shape minsize maxsize is_smallcaps);
+    return $SHAPE{$key};
+}
 
-    my (%seen, $err_details);
+
+# --------------------------------------------------------------------------
+#   Returns a list of all known shapes, in an order suitable for searching.
+# --------------------------------------------------------------------------
+sub get_all_shapes {
+    return @allshapes;
+}
+
+
+# --------------------------------------------------------------------------
+#   Returns mappings of NFSS codes to weight and width, and vice versa.
+# --------------------------------------------------------------------------
+sub map_nfss_codes {
+    my $fontlist = shift;
+
+    my (%weight, %width);
     for my $font (@{$fontlist}) {
-        my $key = join "\x00", @{$font}{@attributes};
+        $weight{ $font->{weight} } //= $font->{weight_class};
+        $width{  $font->{width} }  //= $font->{width_class};
+    }
 
-        if ($seen{$key}) {
-            $err_details .= sprintf $ERR_DETAIL,
-                                    $seen{$key}{filename},
-                                    $font->{filename},
-                                    @{$font}{@attributes};
+    my $from_nfss = {
+        weight => {},
+        width => {},
+    };
+
+    for my $nfssweight (get_all_nfss_weights()) {
+        $from_nfss->{weight}{$nfssweight}
+            = [ grep { $weight{$_} } @{$WEIGHT{$nfssweight}} ];
+    }
+
+    # Some trickery to handle the case where the ul/ub codes are mapped
+    # but the el/eb codes are still empty. We try two things:
+    # 1.  if there is a Thin (Heavy) weight and this is less extreme
+    #     than the weight mapped to ul (ub), we map Thin (Heavy) to ul (ub)
+    # 2.  otherwise we move the ul/ub weight to the el/eb position,
+    #     unless that weight is the Ultralight/Ultrabold weight
+    if (!$ARGV{el} and !$ARGV{ul}) {
+        if (@{$from_nfss->{weight}{ul}}
+                and !@{$from_nfss->{weight}{el}}) {
+            if ($weight{thin}
+                    and $weight{thin}
+                        > $weight{$from_nfss->{weight}{ul}[0]}) {
+                $from_nfss->{weight}{el} = ['thin',];
+            }
+            elsif ($from_nfss->{weight}{ul}[0] ne 'ultralight') {
+                $from_nfss->{weight}{el}
+                    = [ shift @{$from_nfss->{weight}{ul}} ];
+            }
         }
-        else {
-             $seen{$key} = $font;
+    }
+    if (!$ARGV{eb} and !$ARGV{ub}) {
+        if (@{$from_nfss->{weight}{ub}}
+                and !@{$from_nfss->{weight}{eb}}) {
+            if ($weight{heavy}
+                    and $weight{heavy}
+                        < $weight{$from_nfss->{weight}{ub}[0]}) {
+                $from_nfss->{weight}{eb} = ['heavy',]
+                    unless @{$from_nfss->{weight}{b}}
+                       and $weight{$from_nfss->{weight}{b}[0]}
+                           > $weight{heavy};
+            }
+            elsif ($from_nfss->{weight}{ub}[0] ne 'ultrabold') {
+                $from_nfss->{weight}{eb}
+                    = [ shift @{$from_nfss->{weight}{ub}} ];
+            }
         }
     }
 
-    # Die with detailed error message if the font infos aren't unique.
-    if ($err_details) {
-        $log->log($err_details, $ERR_PARSE);
-        die $err_details, $ERR_PARSE;
+    # Special case: if we don't have Regular but we *do* have Medium,
+    # move Medium from the "sb" list to the "m" (i.e., Regular) one.
+    if (!@{$from_nfss->{weight}{''}}) {
+        my $alternate = ( grep { $weight{$_} } qw(medium 500) )[0];
+        if ($alternate) {
+            $from_nfss->{weight}{''} = [$alternate];
+            $from_nfss->{weight}{sb}
+                = [ grep { $_ ne $alternate } @{$from_nfss->{weight}{sb}} ];
+        }
     }
 
-    return 1;
+    # Some more trickery to map the sl code to Book or Text (but of course
+    # only if sl is empty and Book/Text is lighter than Regular)
+    if (!@{$from_nfss->{weight}{sl}}) {
+        $from_nfss->{weight}{sl}
+            = [ grep { $weight{$_} < $weight{$from_nfss->{weight}{''}[0]} }
+                     @{$from_nfss->{weight}{''}} ];
+    }
+
+    NFSSWIDTH:
+    for my $nfsswidth (get_all_nfss_widths()) {
+        for my $width ( @{$WIDTH{$nfsswidth}} ) {
+            if ($width{$width}) {
+                $from_nfss->{width}{$nfsswidth} = [$width];
+                next NFSSWIDTH;
+            }
+        }
+        $from_nfss->{width}{$nfsswidth} = [];
+    }
+
+    # Reverse the %from_nfss mapping to get %to_nfss
+    my %to_nfss;
+    NFSSWEIGHT:
+    for my $nfssweight (get_all_nfss_weights()) {
+        next unless @{$from_nfss->{weight}{$nfssweight}};
+        my $weight = $from_nfss->{weight}{$nfssweight}[0];
+        $weight = "" if $weight eq 'regular';
+
+        NFSSWIDTH:
+        for my $nfsswidth (get_all_nfss_widths()) {
+            my $nfssseries = ($nfssweight . $nfsswidth) || 'm';
+
+            next unless @{$from_nfss->{width}{$nfsswidth}};
+            my $width = $from_nfss->{width}{$nfsswidth}[0];
+            $width = "" if $width eq 'regular';
+            my $series = ($weight . $width) || 'regular';
+            $to_nfss{$series} = $nfssseries;
+        }
+    }
+
+    return [ $from_nfss, \%to_nfss ];
 }
 
 
@@ -248,13 +550,13 @@
 ############################################################################
 
 
-package Font::Info;
+package Font;
 
 # --------------------------------------------------------------------------
-#   Constructor: returns a new (mostly empty) Font::Info object.
+#   Constructor: returns a new Font object.
 # --------------------------------------------------------------------------
-sub new {
-    my ($cls, $filename) = @_;
+sub _new {
+    my ($class, $filename) = @_;
 
     my $self = {
         filename     => $filename,
@@ -266,24 +568,60 @@
         is_smallcaps => 0,
         weight_class => 0,
         width_class  => 0,
+        nfss         => 'rm',
     };
 
-    my ($ext) = $filename =~ m/[.] ([^.]+) \z/xmsi;
-    $ext = lc $ext;
-    if    ($ext eq 'otf') { $self->{fonttype} = 'opentype' }
-    elsif ($ext eq 'ttf') { $self->{fonttype} = 'truetype' }
-    else {
-        die "[ERROR]     Unknown font type '.$ext' ($filename)";
-    }
+    return bless $self, $class;
+}
 
-    return bless $self, $cls;
+
+# --------------------------------------------------------------------------
+#   Factory function: parses a font's metadata and creates a Font object.
+# --------------------------------------------------------------------------
+sub parse {
+    my $filename = shift;
+
+    my $self = __PACKAGE__->_new($filename);
+
+    my $metadata = _get_metadata($filename);
+
+    $self->_parse_metadata($metadata)
+         ->_parse_os2data()
+         ->_parse_featuredata()
+         ->_parse_sizedata()
+         ->_parse_nfss_classification()
+         ->_parse_fonttype()
+         ;
+
+    return $self;
 }
 
 
 # --------------------------------------------------------------------------
-#   Processes the basic info (given as a list of key-value pairs) for a font.
+#   Returns the output of otfinfo -i as a list of key-value pairs.
 # --------------------------------------------------------------------------
-sub process_basicinfo {
+sub _get_metadata {
+    my $filename = shift;
+
+    my $cmd = qq(otfinfo --info "$filename");
+    open my $otfinfo, '-|', $cmd
+        or die "[ERROR]     Could not fork(): $!";
+    my %data = map { my ($k,$v) = m/\A\s* ([^:]+?) \s*:\s* ([^\r\n]+)/xms;
+                     $k =~ s/\s+//xmsg;
+                     (lc $k => $v);
+                   }
+                   grep { m/\A\s* [^:]+? \s*:\s* [^\r\n]+/xms } <$otfinfo>;
+    close $otfinfo
+        or die "[ERROR]     '$cmd' failed.";
+
+    return \%data;
+}
+
+
+# --------------------------------------------------------------------------
+#   Processes the basic metadata for this font.
+# --------------------------------------------------------------------------
+sub _parse_metadata {
     my ($self, $data) = @_;
 
     $self->{originalfamily} = $data->{family};
@@ -303,6 +641,11 @@
     $data->{family}    =~ s/(\d)/$DIGITS[$1]/xmsge;
     $data->{family}    =~ s/[^A-Za-z]+//xmsg;
 
+    if ($data->{family} =~ m/\A DTL/xms) {
+        $data->{family}    =~ s/\A DTL//xms;
+        $data->{subfamily} =~ s/\A (?: ST | T)//xms;
+    }
+
     # remove Adobe's SmallText size, to avoid mistaking it for Text weight
     $data->{family}    =~ s/(?: SmallText | SmText )\z//xmsi;
     $data->{subfamily} =~ s/(?: SmallText | SmText )\z//xmsi;
@@ -322,7 +665,7 @@
     # 3.  Test the weights 'medium' and 'regular' *last*, since these strings
     #     may also occur in Subfamily without indicating the weight;
     #     so we only take them to mean weight if we find no other hit.
-    my @widths = NFSS::get_all_widths();
+    my @widths = Attr::get_all_widths();
     for my $width (@widths) {
         if ($fullinfo =~ m/$width/xms) {
             $self->{width} = $width;
@@ -331,7 +674,7 @@
             last;
         }
     }
-    my @weights = NFSS::get_all_weights();
+    my @weights = Attr::get_all_weights();
     for my $weight (@weights) {
         if ($fullinfo =~ m/$weight/xms) {
             $self->{weight} = $weight;
@@ -340,7 +683,7 @@
             last;
         }
     }
-    my @shapes = NFSS::get_all_shapes();
+    my @shapes = Attr::get_all_shapes();
     for my $shape (@shapes) {
         if ($fullinfo =~ m/$shape/xms) {
             $self->{shape} = $shape;
@@ -362,9 +705,9 @@
 
     # Take care to unabbreviate weight and width; CondensedUltra fonts
     # might end up as 'ultracondensed' instead of 'ultrablackcondensed'!
-    $self->{width}  = NFSS::unabbreviate($self->{width});
-    $self->{weight} = NFSS::unabbreviate($self->{weight});
-    $self->{shape}  = NFSS::unabbreviate($self->{shape});
+    $self->{width}  = Attr::unabbreviate($self->{width});
+    $self->{weight} = Attr::unabbreviate($self->{weight});
+    $self->{shape}  = Attr::unabbreviate($self->{shape});
 
     # Some font families put small caps into separate families;
     # we merge these into the 'main' family.
@@ -380,6 +723,9 @@
         $self->{subfamily}    = $1;
         $self->{is_smallcaps} = 1;
     }
+    if ($self->{name} =~ m/\A DTL/xms && $self->{subfamily} =~ s/Caps//xms) {
+        $self->{is_smallcaps} = 1;
+    }
     if ($self->{name} =~ m/(.+?) (?: $shapes) \z/xmsi) {
         $self->{is_smallcaps} = 1;
     }
@@ -388,19 +734,19 @@
     $shapes = join '|', Util::sort_desc_length(qw(it italic));
     if ($self->{family} =~ m/(.+?) ($shapes) \z/xmsi
             and ($self->{shape} eq 'regular'
-                    or $self->{shape} eq NFSS::unabbreviate(lc($2)))) {
+                    or $self->{shape} eq Attr::unabbreviate(lc($2)))) {
         $self->{family} = $1;
-        $self->{shape}  = NFSS::unabbreviate(lc($2));
+        $self->{shape}  = Attr::unabbreviate(lc($2));
     }
 
     # Some font families put different widths into separate families;
     # we merge these into the 'main' font family.
-    my $widths = join '|', NFSS::get_all_widths();
+    my $widths = join '|', Attr::get_all_widths();
     if ($self->{family} =~ m/(.+?) ($widths) \z/xmsi
             and ($self->{width} eq 'regular'
-                    or $self->{width} eq NFSS::unabbreviate(lc($2)))) {
+                    or $self->{width} eq Attr::unabbreviate(lc($2)))) {
         $self->{family} = $1;
-        $self->{width}  = NFSS::unabbreviate(lc($2));
+        $self->{width}  = Attr::unabbreviate(lc($2));
     }
 
     # Some font families put unusual weights into separate families;
@@ -407,18 +753,18 @@
     # we merge these into the 'main' font family. But we have to be
     # careful with the word 'Text': this might be part of the family name
     # (i.e., Libre Caslon Text) and should not be mistaken for a weight.
-    my $weights = join '|', NFSS::get_all_weights();
+    my $weights = join '|', Attr::get_all_weights();
     if ($self->{family} =~ m/text \z/xmsi) {
         $weights =~ s/[|]? text//xms;
     }
     if ($self->{family} =~ m/(.+?) ($weights) \z/xmsi
             and ($self->{weight} eq 'regular'
-                    or $self->{weight} eq NFSS::unabbreviate(lc($2)))) {
+                    or $self->{weight} eq Attr::unabbreviate(lc($2)))) {
         $self->{family} = $1;
-        $self->{weight} = NFSS::unabbreviate(lc($2));
+        $self->{weight} = Attr::unabbreviate(lc($2));
     }
 
-    $self->{basicshape} = NFSS::get_nfss_shape($self->{shape});
+    $self->{basicshape} = Attr::to_nfss($self->{shape});
 
     # We define 'series' as 'weight + width'. This matches NFSS,
     # but contradicts how most fonts are named (which is 'width + weight').
@@ -428,32 +774,64 @@
         :                                  $self->{weight} . $self->{width}
         ;
 
-    return;
+    return $self;
 }
 
 
 # --------------------------------------------------------------------------
-#   Processes the usWeightClass and usWidthClass data.
+#   Parses usWeightClass and usWidthClass from the OS/2 table.
 # --------------------------------------------------------------------------
-sub process_classdata {
-    my ($self, $classdata) = @_;
+sub _parse_os2data {
+    my $self = shift;
 
-    $self->{weight_class} = $classdata->{weight_class};
-    $self->{width_class}  = $classdata->{width_class};
+    my $os2_table;
+    eval {
+        my $cmd = qq(otfinfo --dump-table "OS/2" "$self->{filename}");
+        open my $otfinfo, '-|:raw', $cmd
+            or die "could not fork(): $!";
+        $os2_table = do { local $/; <$otfinfo> };
+        close $otfinfo
+            or die "'$cmd' failed";
+    } or warn "[WARNING]   $@";
 
-    return;
+    my ($weight_class, $width_class) = unpack '@4n @6n', $os2_table;
+
+    $self->{weight_class} = $weight_class;
+    $self->{width_class}  = $width_class;
+
+    return $self;
 }
 
 
 # --------------------------------------------------------------------------
-#   Processes the list of features this font supports.
+#   Returns a list of features that this font supports.
+#   We include 'kern' in this list even if there is only a 'kern' table
+#   (but no feature), since otftotfm can also use the (legacy) table.
 # --------------------------------------------------------------------------
-sub process_featuredata {
-    my ($self, $data) = @_;
+sub _parse_featuredata {
+    my $self = shift;
 
-    %{$self->{feature}} = map { $_ => 1 } @$data;
+    my $cmd = qq(otfinfo --features "$self->{filename}");
+    open my $otfinfo, '-|', $cmd
+        or die "[ERROR]     Could not fork(): $!";
+    my @data = map { substr $_, 0, 4 } <$otfinfo>;
+    close $otfinfo
+        or die "[ERROR]     '$cmd' failed.";
 
-    return;
+    $cmd = qq(otfinfo --tables "$self->{filename}");
+    open $otfinfo, '-|', $cmd
+        or die "[ERROR]     Could not fork(): $!";
+    my $data = do { local $/; <$otfinfo> };
+    close $otfinfo
+        or die "[ERROR]     '$cmd' failed.";
+
+    if ($data =~ m/\d+ \s+ kern/xms) {
+        push @data, 'kern';
+    }
+
+    %{$self->{feature}} = map { $_ => 1 } @data;
+
+    return $self;
 }
 
 
@@ -460,11 +838,23 @@
 # --------------------------------------------------------------------------
 #   Extracts 'minsize' and 'maxsize' from the optical design size info.
 # --------------------------------------------------------------------------
-sub process_sizedata {
-    my ($self, $sizedata) = @_;
+sub _parse_sizedata {
+    my $self = shift;
 
-    my ($minsize, $maxsize) = @$sizedata;
+    my $cmd = qq(otfinfo --optical-size "$self->{filename}");
+    open my $otfinfo, '-|', $cmd
+        or die "[ERROR]     Could not fork(): $!";
+    my $data = do { local $/; <$otfinfo> };
+    close $otfinfo
+        or die "[ERROR]     '$cmd' failed.";
 
+    my ($minsize, $maxsize)
+        = $data =~ m/[(] ([\d.]+) \s* pt, \s*
+                         ([\d.]+) \s* pt  \s* [])]/xms;
+
+    $minsize //= 0;
+    $maxsize //= 0;
+
     # fix some known bugs
     if ($self->{name} eq 'GaramondPremrPro-It'
         && $minsize == 6 && $maxsize == 8.9) {
@@ -488,7 +878,7 @@
 
     @{$self}{qw(minsize maxsize)} = ($minsize, $maxsize);
 
-    return;
+    return $self;
 }
 
 
@@ -495,146 +885,126 @@
 # --------------------------------------------------------------------------
 #   Adds the NFSS classification (rm, sf, tt) to self.
 # --------------------------------------------------------------------------
-sub process_nfss_classification {
-    my ($self, $data) = @_;
+sub _parse_nfss_classification {
+    my $self = shift;
 
-    $self->{nfss} = $data;
-
-    return;
-}
-
-
-############################################################################
-
-
-package Font::Raw;
-
-# --------------------------------------------------------------------------
-#   Returns the output of otfinfo -i as a list of key-value pairs.
-# --------------------------------------------------------------------------
-sub get_basicinfo {
-    my $filename = shift;
-
-    my $cmd = qq(otfinfo --info "$filename");
-    open my $otfinfo, '-|', $cmd
-        or die "[ERROR]     Could not fork(): $!";
-    my %data = map { my ($k,$v) = m/\A\s* ([^:]+?) \s*:\s* ([^\r\n]+)/xms;
-                     $k =~ s/\s+//xmsg;
-                     (lc $k => $v);
-                   }
-                   grep { m/\A\s* [^:]+? \s*:\s* [^\r\n]+/xms } <$otfinfo>;
-    close $otfinfo
-        or die "[ERROR]     '$cmd' failed.";
-
-    return \%data;
-}
-
-
-# --------------------------------------------------------------------------
-#   Returns usWeightClass and usWidthClass from the OS/2 table.
-# --------------------------------------------------------------------------
-sub get_classdata {
-    my $filename = shift;
-
-    my $os2_table;
+    my $classification;
     eval {
-        my $cmd = qq(otfinfo --dump-table "OS/2" "$filename");
+        my $cmd = qq(otfinfo --dump-table "post" "$self->{filename}");
         open my $otfinfo, '-|:raw', $cmd
             or die "could not fork(): $!";
-        $os2_table = do { local $/; <$otfinfo> };
+        my $post_table = do { local $/; <$otfinfo> };
         close $otfinfo
             or die "'$cmd' failed";
+
+        my $is_fixed_pitch = unpack '@12N', $post_table;
+
+        $self->{nfss} = $is_fixed_pitch                          ? 'tt'
+                      : $self->{filename} =~ m/mono(?!type)/xmsi ? 'tt'
+                      : $self->{filename} =~ m/sans/xmsi         ? 'sf'
+                      :                                            'rm'
+                      ;
     } or warn "[WARNING]   $@";
 
-    my ($weight_class, $width_class) = unpack '@4n @6n', $os2_table;
-
-    return {
-        weight_class => $weight_class,
-        width_class  => $width_class,
-    };
+    return $self;
 }
 
 
 # --------------------------------------------------------------------------
-#   Returns a list of features that this font supports.
-#   We include 'kern' in this list even if there is only a 'kern' table
-#   (but no feature), since otftotfm can also use the (legacy) table.
+#   Determines the OpenType flavor of this font.
 # --------------------------------------------------------------------------
-sub get_featuredata {
-    my $filename = shift;
+sub _parse_fonttype() {
+    my $self = shift;
 
-    my $cmd = qq(otfinfo --features "$filename");
-    open my $otfinfo, '-|', $cmd
-        or die "[ERROR]     Could not fork(): $!";
-    my @data = map { substr $_, 0, 4 } <$otfinfo>;
-    close $otfinfo
-        or die "[ERROR]     '$cmd' failed.";
+    my ($ext) = $self->{filename} =~ m/[.] ([^.]+) \z/xmsi;
+    $ext = lc $ext;
 
-    $cmd = qq(otfinfo --tables "$filename");
-    open $otfinfo, '-|', $cmd
-        or die "[ERROR]     Could not fork(): $!";
-    my $data = do { local $/; <$otfinfo> };
-    close $otfinfo
-        or die "[ERROR]     '$cmd' failed.";
+    $self->{fonttype}
+        = $ext eq 'otf' ? 'opentype'
+        : $ext eq 'ttf' ? 'truetype'
+        :                 ''
+        ;
 
-    if ($data =~ m/\d+ \s+ kern/xms) {
-        push @data, 'kern';
+    if (!$self->{fonttype}) {
+        open my $fontfile, '<:raw', $self->{filename}
+            or die "[ERROR]     Could not open '$self->{filename}': $!";
+        my $fontsignature;
+        read $fontfile, $fontsignature, 4;
+        $self->{fonttype}
+            = $fontsignature eq 'OTTO'             ? 'opentype'
+            : $fontsignature eq "\x00\x01\x00\x00" ? 'truetype'
+            :                                        ''
+            ;
+        close $fontfile;
     }
 
-    return \@data;
+    if (!$self->{fonttype}) {
+        die "[ERROR]     Unknown font type ($self->{filename})"
+    }
+
+    return $self;
 }
 
 
-# --------------------------------------------------------------------------
-#   Returns the size info for this font (which may be empty).
-# --------------------------------------------------------------------------
-sub get_sizedata {
-    my $filename = shift;
+# Error messages, used in assert_unique().
+my $ERR_DETAIL =<<'END_ERR_DETAIL';
+[ERROR]     I've parsed both %s
+                         and %s as
 
-    my $cmd = qq(otfinfo --optical-size "$filename");
-    open my $otfinfo, '-|', $cmd
-        or die "[ERROR]     Could not fork(): $!";
-    my $data = do { local $/; <$otfinfo> };
-    close $otfinfo
-        or die "[ERROR]     '$cmd' failed.";
+            Family:     %s
+            Weight:     %s
+            Width:      %s
+            Shape:      %s
+            Size:       %s-%s
+            Smallcaps:  %s
 
-    my ($minsize, $maxsize)
-        = $data =~ m/[(] ([\d.]+) \s* pt, \s*
-                         ([\d.]+) \s* pt  \s* [])]/xms;
+END_ERR_DETAIL
 
-    $minsize //= 0;
-    $maxsize //= 0;
-    return [ $minsize, $maxsize ];
-}
+my $ERR_PARSE =<<'END_ERR_PARSE';
+[ERROR]     I failed to parse all fonts in a unique way;
+            presumably some fonts have unusual widths, weights or shapes.
 
+            Try one of the following:
+            -   Run 'autoinst' on a smaller set of fonts,
+                omitting the ones that weren't parsed correctly;
+            -   Add the missing widths, weights or shapes to the tables
+                'WIDTH', 'WEIGHT' or 'SHAPE' in the source code;
 
+            Please also send a bug report to the author.
+END_ERR_PARSE
+
 # --------------------------------------------------------------------------
-#   Returns the NFSS classification (i.e., rm, sf or tt) for this font.
-#   Note that the algorithm used is "best effort only", so its results
-#   may be wrong.
+#   Asserts all parsed fonts are unique.
 # --------------------------------------------------------------------------
-sub get_nfss_classification {
-    my $filename = shift;
+sub assert_unique {
+    my ($log, $fontlist) = @_;
 
-    my $classification;
-    eval {
-        my $cmd = qq(otfinfo --dump-table "post" "$filename");
-        open my $otfinfo, '-|:raw', $cmd
-            or die "could not fork(): $!";
-        my $post_table = do { local $/; <$otfinfo> };
-        close $otfinfo
-            or die "'$cmd' failed";
+    # These attributes should uniquely identify each font.
+    my @attributes
+        = qw(family weight width shape minsize maxsize is_smallcaps);
 
-        my $is_fixed_pitch = unpack '@12N', $post_table;
+    my (%seen, $err_details);
+    for my $font (@{$fontlist}) {
+        my $key = join "\x00", @{$font}{@attributes};
 
-        $classification = $is_fixed_pitch                  ? 'tt'
-                        : $filename =~ m/mono(?!type)/xmsi ? 'tt'
-                        : $filename =~ m/sans/xmsi         ? 'sf'
-                        :                                    'rm'
-                        ;
-    } or warn "[WARNING]   $@";
+        if ($seen{$key}) {
+            $err_details .= sprintf $ERR_DETAIL,
+                                    $seen{$key}{filename},
+                                    $font->{filename},
+                                    @{$font}{@attributes};
+        }
+        else {
+             $seen{$key} = $font;
+        }
+    }
 
-    return $classification;
+    # Die with detailed error message if the font infos aren't unique.
+    if ($err_details) {
+        $log->log($err_details, $ERR_PARSE);
+        die $err_details, $ERR_PARSE;
+    }
+
+    return 1;
 }
 
 
@@ -1581,13 +1951,13 @@
     my ($self, $nfss_mapping) = @_;
 
     print {$self} "\n" . '-' x 76 . "\n\nNFSS mappings:\n\n";
-    for my $weight (NFSS::get_all_nfss_weights()) {
+    for my $weight (Attr::get_all_nfss_weights()) {
         printf {$self} "    %-3s =>  %s\n",
                        $weight || 'm',
                        $nfss_mapping->{weight}{$weight}[0] || '';
     }
     printf {$self} "\n";
-    for my $width (NFSS::get_all_nfss_widths()) {
+    for my $width (Attr::get_all_nfss_widths()) {
         printf {$self} "    %-3s =>  %s\n",
                        $width || 'm',
                        $nfss_mapping->{width}{$width}[0] || '';
@@ -1654,434 +2024,6 @@
 ############################################################################
 
 
-package NFSS;
-
-=begin Comment
-
-    Some fontnames contain abbreviated words for width, weight and/or shape;
-    we unabbreviate these using the following table.
-
-=end Comment
-
-=cut
-
-my %FULL_FORM = (
-    cmp     =>  'compressed',
-    comp    =>  'compressed',
-    cond    =>  'condensed',
-    demi    =>  'demibold',
-    extcond =>  'extracondensed',
-    hair    =>  'hairline',
-    incline =>  'inclined',
-    it      =>  'italic',
-    ita     =>  'italic',
-    md      =>  'medium',
-    slant   =>  'slanted',
-    ultra   =>  'ultrablack',
-);
-
-=begin Comment
-
-    LaTeX's NFSS contains a number of standard codes for weight and width:
-    - weight: ul, el, l, sl, m, sb, b, eb, ub
-    - width:  uc, ec, c, sc, m, sx, x, ex, ux
-
-    These codes are not always a perfect match with the weights and widths
-    present in a font family; some families (especially many sans serif ones)
-    contain more or different weights and widths, and the naming of those
-    weights and widths isn't always consistent between font families.
-    To handle this situation, we use a two-tiered approach:
-    1.  We install all fonts using a "series" name that is the concatenation
-        of whatever the font designer has chosen to call the weight and width
-        (but in all *lower*case).
-    2.  We add "alias" rules to the .fd files that map the standard NFSS codes
-        to actual fonts.
-
-    In step 1, we follow NFSS in leaving out any occurrence of
-    the word "regular" unless *both* weight and width are Regular;
-    in that case, the 'series' attribute becomes "regular".
-
-    The two tables WEIGHT and WIDTH are used to control step 2.
-    It contains several entries of the form
-
-        sc  =>  [ qw( semicondensed narrow ) ],
-
-    This should be read as follows: the NFSS code "sc" is mapped to
-    the *first* width on the right hand side present in the current family.
-
-    Please note that the tables contain empty keys instead of "m" for the
-    regular weight and width. NFSS actually combines weight and width into
-    a single "series" attribute; a weight or width of "m" is left out of
-    this combination (unless *both* weight and width are equal to "m"; then
-    the series becomes "m", but that's a special case we deal with later on).
-
-    In addition to the mapping of NFSS codes, the two mentioned tables are
-    also used in parsing the font's metadata to determine its weight and
-    width: any string that occurs on the right hand side is considered a
-    possible name to be searched for.
-
-    These tables can be extended to teach autoinst about new weights or
-    widths.  Suppose your font family contains a "Hemibold" weight, that
-    you want mapped to the "sb" code. Then add the name "hemibold" to
-    the right hand side of the "sb" entry in the WEIGHT table:
-
-        sb  =>  [ qw( semibold demibold medium hemibold ) ],
-
-    In this case, since it's in last position, it's only mapped to "sb"
-    if none of the other fonts are present. Put it earlier in the list
-    to give it higher priority.
-
-    Note that autoinst converts all metadata to lowercase to avoid
-    inconsistent capitalization; so all entries in these tables should
-    be *lowercase* as well.
-
-    Technical notes:
-    -   We define WEIGHT and WIDTH first as arrays
-        and then as hashtables; this allows us to use the array-variants
-        as an *ordered* (by weight/width) list of values (in the routines
-        get_all_nfss_weights and get_all_nfss_widths).
-
-=end Comment
-
-=cut
-
-my @WEIGHT = (
-    ul  =>  [ qw( ultralight thin 100 hairline ) ],
-    el  =>  [ qw( extralight 200 ) ],
-    l   =>  [ qw( light 300 ) ],
-    sl  =>  [ qw( semilight blond ) ],
-    ''  =>  [ qw( regular normal text book 400 ) ],
-    sb  =>  [ qw( semibold demibold 600 medium 500 ) ],
-    b   =>  [ qw( bold 700 ) ],
-    eb  =>  [ qw( extrabold 800 ) ],
-    ub  =>  [ qw( ultrabold black heavy extrablack ultrablack 900 fatface
-                  ultraheavy poster super 1000 ) ],
-);
-
-my @WIDTH = (
-    uc  =>  [ qw( ultracondensed extracompressed ultracompressed ) ],
-    ec  =>  [ qw( extracondensed compressed compact ) ],
-    c   =>  [ qw( condensed ) ],
-    sc  =>  [ qw( semicondensed narrow ) ],
-    ''  =>  [ qw( regular ) ],
-    sx  =>  [ qw( semiextended semiexpanded wide ) ],
-    x   =>  [ qw( extended expanded ) ],
-    ex  =>  [],
-    ux  =>  [],
-);
-
-=begin Comment
-
-    The SHAPE table maps various shape names to NFSS codes.
-
-    Like in the other * tables, entries may be added to teach autoinst
-    about new shapes. Note that this table is "the other way around"
-    compared to WEIGHT and WIDTH; those map NFSS codes to names,
-    this one maps names to NFSS codes. That's because the data from
-    this table is used in a slightly different way; notably, it isn't
-    used in the map_nfss_codes routine.
-
-=end Comment
-
-=cut
-
-my %SHAPE = (
-    roman       =>  'n',
-    upright     =>  'n',
-    italic      =>  'it',
-    inclined    =>  'sl',
-    oblique     =>  'sl',
-    slanted     =>  'sl',
-    romani      =>  'n',    # Silentium has two roman shapes, but no italic;
-    romanii     =>  'it',   # so we cheat by mapping the second roman to 'it'
-);
-
-
-####    FUNCTIONS FOR ACCESSING DATA FROM THE TABLES    ####
-
-
-# --------------------------------------------------------------------------
-#   Returns the unabbreviated form of its argument,
-#   or its argument itself if no unabbreviated form is known.
-# --------------------------------------------------------------------------
-sub unabbreviate {
-    my $key = shift;
-
-    return $FULL_FORM{$key} // $key;
-}
-
-
-# Auxiliary table that reverses FULL_FORM; maps full forms to abbrevs.
-my %abbrev;
-while (my ($k, $v) = each %FULL_FORM) {
-    push @{$abbrev{$v}}, $k;
-}
-for my $full (keys %abbrev) {
-    push @{$abbrev{$full}}, $full;
-}
-
-
-# --------------------------------------------------------------------------
-#   Returns a list of known abbreviations of its argument,
-#   or a singleton list containing just the argument itself
-#   if no abbreviations are known.
-# --------------------------------------------------------------------------
-sub get_abbreviated_forms {
-    my $key = shift;
-
-    return @{ $abbrev{$key} // [$key] };
-}
-
-
-my %WEIGHT = @WEIGHT;
- at WEIGHT = grep { !ref } @WEIGHT;
-
-# Add abbreviated forms, using the %ABBREV table constructed earlier.
-for my $code (@WEIGHT) {
-    $WEIGHT{$code}
-        = [ map { get_abbreviated_forms($_) } @{$WEIGHT{$code}} ];
-}
-
-
-# --------------------------------------------------------------------------
-#   Returns all weight names that might map to the given NFSS code.
-# --------------------------------------------------------------------------
-sub get_weights {
-    my $key = shift;
-
-    return @{$WEIGHT{$key}};
-}
-
-
-# --------------------------------------------------------------------------
-#   Adds weight names to the WEIGHT table.
-# --------------------------------------------------------------------------
-sub set_weights {
-    my ($key, @values) = @_;
-
-    $WEIGHT{$key} = [ @values, @{$WEIGHT{$key}} ];
-    return;
-}
-
-
-# --------------------------------------------------------------------------
-#   Returns a list of NFSS weights, sorted from light to heavy.
-# --------------------------------------------------------------------------
-sub get_all_nfss_weights {
-    return @WEIGHT;
-}
-
-
-my @allweights = grep { $_ !~ m/ medium | regular | text /xms }
-                      map { @{$_} } values %WEIGHT;
- at allweights = (Util::sort_desc_length(@allweights), qw(medium regular text));
-
-# --------------------------------------------------------------------------
-#   Returns a list of all known weight names,
-#   in an order that's suitable for search routines.
-# --------------------------------------------------------------------------
-sub get_all_weights {
-    return @allweights;
-}
-
-my %WIDTH = @WIDTH;
- at WIDTH = grep { !ref } @WIDTH;
-
-# Add abbreviated forms, using the %ABBREV table constructed earlier.
-for my $code (@WIDTH) {
-    $WIDTH{$code}
-        = [ map { get_abbreviated_forms($_) } @{$WIDTH{$code}} ];
-}
-
-
-# --------------------------------------------------------------------------
-#   Returns all width names that might map to the given NFSS code.
-# --------------------------------------------------------------------------
-sub get_widths {
-    my $key = shift;
-
-    return @{$WIDTH{$key}};
-}
-
-
-# --------------------------------------------------------------------------
-#   Adds width names to the WEIGHT table.
-# --------------------------------------------------------------------------
-sub set_widths {
-    my ($key, @values) = @_;
-
-    $WIDTH{$key} = [ @values, @{$WIDTH{$key}} ];
-    return;
-}
-
-
-# --------------------------------------------------------------------------
-#   Returns a list of NFSS widths, sorted from narrow to wide.
-# --------------------------------------------------------------------------
-sub get_all_nfss_widths {
-    return @WIDTH;
-}
-
-
-my @allwidths = grep { $_ ne 'regular' } map { @{$_} } values %WIDTH;
- at allwidths = Util::sort_desc_length(@allwidths);
-
-# --------------------------------------------------------------------------
-#   Returns a list of all known width names,
-#   in an order that's suitable for search routines.
-# --------------------------------------------------------------------------
-sub get_all_widths {
-    return @allwidths;
-}
-
-
-# Add abbreviated forms to %SHAPE table.
-for my $full (keys %abbrev) {
-    if (defined $SHAPE{$full}) {
-        for my $abbrev (get_abbreviated_forms($full)) {
-            $SHAPE{$abbrev} = $SHAPE{$full};
-        }
-    }
-}
-
-# --------------------------------------------------------------------------
-#   Returns the NFSS code for the given shape.
-# --------------------------------------------------------------------------
-sub get_nfss_shape {
-    my $key = shift;
-
-    return $SHAPE{$key};
-}
-
-
-my @allshapes = Util::sort_desc_length(keys %SHAPE);
-
-# --------------------------------------------------------------------------
-#   Returns a list of all known shape names,
-#   in an order that's suitable for search routines.
-# --------------------------------------------------------------------------
-sub get_all_shapes {
-    return @allshapes;
-}
-
-# --------------------------------------------------------------------------
-#   Returns a mapping of NFSS codes to weight and width names.
-# --------------------------------------------------------------------------
-sub map_nfss_codes {
-    my $fontlist = shift;
-
-    my (%weight, %width);
-    for my $font (@{$fontlist}) {
-        $weight{ $font->{weight} } //= $font->{weight_class};
-        $width{ $font->{width} }   //= $font->{width_class};
-    }
-
-    my $mapping = {
-        weight => {},
-        width => {},
-    };
-
-    for my $nfssweight (NFSS::get_all_nfss_weights()) {
-        $mapping->{weight}{$nfssweight}
-            = [ grep { $weight{$_} } NFSS::get_weights($nfssweight) ];
-    }
-
-    # Some trickery to handle the case where the ul/ub codes are mapped
-    # but the el/eb codes are still empty. We try two things:
-    # 1.  if there is a Thin (Heavy) weight and this is less extreme
-    #     than the weight mapped to ul (ub), we map Thin (Heavy) to ul (ub)
-    # 2.  otherwise we move the ul/ub weight to the el/eb position,
-    #     unless that weight is the Ultralight/Ultrabold weight
-    if (!$ARGV{el} and !$ARGV{ul}) {
-        if (@{$mapping->{weight}{ul}} and !@{$mapping->{weight}{el}}) {
-            if ($weight{thin}
-                    and $weight{thin} > $weight{$mapping->{weight}{ul}[0]}) {
-                $mapping->{weight}{el} = ['thin',];
-            }
-            elsif ($mapping->{weight}{ul}[0] ne 'ultralight') {
-                $mapping->{weight}{el} = [ shift @{$mapping->{weight}{ul}} ];
-            }
-        }
-    }
-    if (!$ARGV{eb} and !$ARGV{ub}) {
-        if (@{$mapping->{weight}{ub}} and !@{$mapping->{weight}{eb}}) {
-            if ($weight{heavy}
-                    and $weight{heavy} < $weight{$mapping->{weight}{ub}[0]}) {
-                $mapping->{weight}{eb} = ['heavy',]
-                    unless @{$mapping->{weight}{b}}
-                       and $weight{$mapping->{weight}{b}[0]} > $weight{heavy};
-            }
-            elsif ($mapping->{weight}{ub}[0] ne 'ultrabold') {
-                $mapping->{weight}{eb} = [ shift @{$mapping->{weight}{ub}} ];
-            }
-        }
-    }
-
-    # Special case: if we don't have Regular but we *do* have Medium,
-    # move Medium from the "sb" list to the "m" (i.e., Regular) one.
-    if (!@{$mapping->{weight}{''}}) {
-        my $alternate = ( grep { $weight{$_} } qw(medium 500) )[0];
-        if ($alternate) {
-            $mapping->{weight}{''} = [$alternate];
-            $mapping->{weight}{sb}
-                = [ grep { $_ ne $alternate } @{$mapping->{weight}{sb}} ];
-        }
-    }
-
-    # Some more trickery to map the sl code to Book or Text (but of course
-    # only if sl is empty and Book/Text is lighter than Regular)
-    if (!@{$mapping->{weight}{sl}}) {
-        $mapping->{weight}{sl}
-            = [ grep { $weight{$_} < $weight{$mapping->{weight}{''}[0]} }
-                     @{$mapping->{weight}{''}} ];
-    }
-
-    NFSSWIDTH:
-    for my $nfsswidth (NFSS::get_all_nfss_widths()) {
-        for my $width ( NFSS::get_widths($nfsswidth) ) {
-            if ($width{$width}) {
-                $mapping->{width}{$nfsswidth} = [$width];
-                next NFSSWIDTH;
-            }
-        }
-        $mapping->{width}{$nfsswidth} = [];
-    }
-
-    return $mapping;
-}
-
-# --------------------------------------------------------------------------
-#   Returns a mapping of weight and width names to NFSS codes.
-# --------------------------------------------------------------------------
-sub invert_mapping {
-    my $nfss_mapping = shift;
-
-    my %to_nfss;
-    NFSSWEIGHT:
-    for my $nfssweight (NFSS::get_all_nfss_weights()) {
-        next unless @{$nfss_mapping->{weight}{$nfssweight}};
-        my $weight = $nfss_mapping->{weight}{$nfssweight}[0];
-        $weight = "" if $weight eq 'regular';
-
-        NFSSWIDTH:
-        for my $nfsswidth (NFSS::get_all_nfss_widths()) {
-            my $nfssseries = ($nfssweight . $nfsswidth) || 'm';
-
-            next unless @{$nfss_mapping->{width}{$nfsswidth}};
-            my $width = $nfss_mapping->{width}{$nfsswidth}[0];
-            $width = "" if $width eq 'regular';
-            my $series = ($weight . $width) || 'regular';
-            $to_nfss{$series} = $nfssseries;
-        }
-    }
-
-    return \%to_nfss;
-}
-
-
-############################################################################
-
-
 package Options;
 
 my $USAGE =<<'END_USAGE';
@@ -2228,7 +2170,7 @@
                                           "$key=$values";
                                      my @values = split m/,/, lc $values;
                                      $key = q{} if $key eq 'm';
-                                     NFSS::set_weights($key, @values);
+                                     Attr::set_weight($key, @values);
                                      $ARGV{$key} = 'user-defined';
                                  },
         'nfsswidth=s%'        => sub {
@@ -2238,7 +2180,7 @@
                                      push @{$ARGV{nfsswidth}},
                                           "$key=$values";
                                      $key = q{} if $key eq 'm';
-                                     NFSS::set_widths($key, @values);
+                                     Attr::set_width($key, @values);
                                      $ARGV{$key} = 'user-defined';
                                  },
         'math!'               => \$ARGV{math},
@@ -2438,7 +2380,7 @@
         code  => { n => 'sc', it => 'scit', sl => 'scsl' },
         reqd  => [ 'smcp' ],
         nice  => [],
-        extra => '--unicoding="germandbls =: SSsmall"',
+        extra => '--unicoding="germandbls =: SSsmall" --unicoding="ff =: FFsmall" --unicoding="fi =: FIsmall" --unicoding="ffi =: FFIsmall" --unicoding="fl =: FLsmall" --unicoding="ffl =: FFLsmall"',
         name  => 'sc',
     },
     swash => {
@@ -2738,7 +2680,7 @@
     my @encodings = split /,/, $ARGV{encoding};
     for my $enc (@encodings) {
         if ($enc !~ m/\A(OT1|T1|TS1|LY1|LGR|T2[ABC]|T3|TS3)\z/xmsi) {
-            my $try = Cwd::abs_path($enc);
+            my $try = $enc;
             $try .= '.enc' if $try !~ m/[.]enc\z/xms;
             if (!-e $try) {
                 die "[ERROR]     No .enc file found for '$enc'";
@@ -2753,7 +2695,7 @@
     # - the text encodings contain T1 and the user didn't turn off TS1
     if ($ARGV{textcomp} == 1
             or ($ARGV{textcomp} >= 1
-                and grep { $_ =~ m/T1/xmsi } @{$ARGV{encoding}})) {
+                and grep { uc $_ eq 'T1' } @{$ARGV{encoding}})) {
         $ARGV{textcomp} = 1;
     }
     else {
@@ -3106,8 +3048,10 @@
         return;
     }
 
-    # Figure out which encoding file to use for this font.
-    my $try = Cwd::abs_path($workitem->{encoding});
+    # Figure out whether the encoding option refers to a custom encoding file
+    # in the current directory, or to the name of one of our standard
+    # encodings.  In the latter case, we add the 'fontools_' prefix.
+    my $try = $workitem->{encoding};
     $try .= '.enc' if $try !~ m/[.]enc\z/xms;
     if (-e $try) {
         $workitem->{enc_file} = $try;
@@ -3171,7 +3115,7 @@
         $ARGV{typeface} || $family);
 
     # The otftotfm-generated map file refers to the original otf files;
-    # we need to change this to use our name-modifyd Type1 fonts instead.
+    # we need to change this to use our name-modified Type1 fonts instead.
     my $mapfile = File::Spec->catfile(
         $ARGV{target},
         'fonts',
@@ -4053,7 +3997,7 @@
 
 =head1 VERSION
 
-This document describes B<autoinst> version 20200729.
+This document describes B<autoinst> version 20201218.
 
 
 =head1 RECENT CHANGES
@@ -4062,6 +4006,14 @@
 
 =over 12
 
+=item I<2020-12-18>
+
+Fixed a problem with files not being found on Windows.
+Added extra C<--unicoding> options to prevent getting
+lowercase f-ligatures in smallcaps for some buggy fonts.
+Optimized font info parsing for DTL and TypeBy font families.
+Cleaned up the code for better maintainability.
+
 =item I<2020-07-29>
 
 Some changes in the generated F<sty> and F<fd> files,

Modified: trunk/Master/texmf-dist/scripts/fontools/ot2kpx
===================================================================
--- trunk/Master/texmf-dist/scripts/fontools/ot2kpx	2020-12-19 21:25:38 UTC (rev 57170)
+++ trunk/Master/texmf-dist/scripts/fontools/ot2kpx	2020-12-19 21:26:11 UTC (rev 57171)
@@ -38,7 +38,7 @@
 use List::Util @List::Util::EXPORT_OK;
 use Pod::Usage;
 
-my $VERSION = "20200729";
+my $VERSION = "20201218";
 
 our ($NUM_GLYPHS, $UNITS_PER_EM, %kern);
 
@@ -858,7 +858,7 @@
 
 =head1 VERSION
 
-This document describes B<ot2kpx> version 20200729.
+This document describes B<ot2kpx> version 20201218.
 
 
 =head1 RECENT CHANGES



More information about the tex-live-commits mailing list.