#!/usr/bin/perl
use strict;
use Font::Fret;
use Font::TTF::Useall;

fret('Attach', @ARGV);

package Attach;

use XML::Parser::Expat;
use Getopt::Std;
use Pod::Usage;
use IO::File;

our (@ISA, $VERSION);

BEGIN {
    @ISA = qw(Font::Fret::Default);
    $VERSION = "1.000";
      }

my ($opts, $apRE);

my ($gid, @xml_dat, $fontname, $fontupem, @rev, @macrev, $usort, $psort);

sub make_cids
{
    my ($class, $font) = @_;
    my (@map, @res, $m, $pname);
    my ($c) = $font->{'cmap'}->read->find_ms;

    if ($opts->{'a'})
    {
        my ($xml) = XML::Parser::Expat->new();
        $xml->setHandlers('Start' => sub {
            my ($xml, $tag, %attrs) = @_;

            if ($tag eq 'glyph')
            {
                $gid = $attrs{'GID'} || $c->{'val'}{hex($attrs{'UID'})}
                    || $font->{'post'}{'STRINGS'}{$attrs{'PSName'}};
                if ($gid == 0 && ($attrs{'PSName'} || $attrs{'UID'}))
                { return $xml->xpcarp("No glyph called: $attrs{'PSName'}, Unicode: $attrs{'UID'}"); }
                $xml_dat[$gid]{'ps'} = $attrs{'PSName'};
                $xml_dat[$gid]{'UID'} = $attrs{'UID'};
            } elsif ($tag eq 'point')
            {
                $pname = $attrs{'type'} =~ /$apRE/oi ? $attrs{'type'} : undef;;
            } elsif ($tag eq 'contour')
            {
                $xml_dat[$gid]{'points'}{$pname}{'cont'} = $attrs{'num'} if $pname;
            } elsif ($tag eq 'location')
            {
                $xml_dat[$gid]{'points'}{$pname}{'loc'} = [$attrs{'x'}, $attrs{'y'}] if $pname;
            } elsif ($tag eq 'font')
            {
                $fontname = $attrs{'name'};
                $fontupem = $attrs{'upem'};
            } elsif ($tag eq 'compound')
            {
                push (@{$xml_dat[$gid]{'compounds'}}, {%attrs});
            } elsif ($tag eq 'break')
            {
                $xml_dat[$gid]{'break'} = $attrs{'weight'};
            }
        });

        $xml->parsefile($opts->{'a'}) || die "Can't parse " . $opts->{'a'};
    }

    @rev = $font->{'cmap'}->read->reverse('array' => 1);
    @map = $font->{'cmap'}->read->reverse;
    $map[$font->{'maxp'}{'numGlyphs'}] = 0;
    foreach $m (@map) { $m = 65536 if $m == 0; }
    @res = (sort {($usort && $map[$a] <=> $map[$b]) || ($psort && $font->{'post'}{'VAL'}[$a] cmp $font->{'post'}{'VAL'}[$b]) || $a <=> $b}
            (0 .. $font->{'maxp'}{'numGlyphs'} - 1));

    return ("Glyph ID", @res);
}

sub boxhdr
{
    return ("Advance", "PSName", "GID", "Unicode");
}

sub lowdat
{
    my ($class, $cid, $gid, $glyph, $uid, $font) = @_;

    return ($font->{'hmtx'}{'advance'}[$gid], "r|$font->{'post'}{'VAL'}[$gid]");
}

sub topdat
{
    my ($class, $cid, $gid, $glyph, $uid, $font) = @_;

    return ($gid, "r,r|" . join(',', map {sprintf("|U+%04X", $_)} sort {$a <=> $b} (defined $rev[$gid] ? @{$rev[$gid]} : ())));
}

sub row1hdr
{
    my ($class, $font) = @_;
    my ($i);

    for ($i = 0; $i < $font->{'cmap'}{'Num'}; $i++)
    {
        if ($font->{'cmap'}{'Tables'}[$i]{'Platform'} == 1)
        { @macrev = $font->{'cmap'}->reverse('tnum' => $i); last; }
    }

    return ('PSName', 'GID', 'UID', 'Macid',
            'r,b|adv', 'r,i|xmax', 'r,i|xmin', 'r,i|ymax', 'r,i|ymin');
}

sub row1
{
    my ($class, $cid, $gid, $glyph, $uid, $font) = @_;
    my ($aw) = $font->{'hmtx'}{'advance'}[$gid];
    my ($rsb) = $aw - $glyph->{'xMax'};

    return ($font->{'post'}{'VAL'}[$gid], $gid, 
            join(',', map {sprintf("|U+%04X", $_)} sort {$a <=> $b} (defined $rev[$gid] ? @{$rev[$gid]} : ())), $macrev[$gid],
            "r,b|$aw", "r,i|$glyph->{'xMax'}",
            "r,i|$glyph->{'xMin'}", "r,i|$glyph->{'yMax'}",
            "r,i|$glyph->{'yMin'}");
}

sub row2hdr
{
    my ($class, $font) = @_;

    return ("Attach1", "Attach2", "Attach3", "Attach4");
}

sub row2
{
    my ($class, $cid, $gid, $glyph, $uid, $font) = @_;
    my ($ptext);
    my ($p, $pc, @res);

    if (!defined $xml_dat[$gid]{'points'})
    { return (@res); }
    
    $glyph->get_points if defined $glyph;
    foreach $p (sort keys %{$xml_dat[$gid]{'points'}})
    {
    	my $res;
        $pc = $xml_dat[$gid]{'points'}{$p};
        if (exists $pc->{'cont'})
        {
	        my $pnum = $glyph->{'endPoints'}[$pc->{'cont'}];
	        $res = "$p($pc->{'cont'}:";
#	        $res = "$p($pc->{'cont'},$pnum)($pc->{'loc'}[0],$pc->{'loc'}[1])";
	        $res .= "$glyph->{'x'}[$pnum],$glyph->{'y'}[$pnum])";
#	        $res = "$p($pc->{'cont'},$pnum)";
		}
		elsif (exists $pc->{'loc'})
		{
			$res = "$p($pc->{'loc'}[0],$pc->{'loc'}[1])";
		}
        push (@res, $res);
    }
    return (@res);
}

my $specialfh;

sub process_argv
{
	my ($self);
	($self, $opts) = @_;
	getopts("a:bd:fgh:i:m:o:p:qrs:?", $opts);
	
	if ($opts->{'?'})
    {
        pod2usage( -verbose => 2, -noperldoc => 1);
    }
    unless ($ARGV[0])
    {
		pod2usage (1);
	}
	if ($opts->{'i'})
	{
		$apRE = qr/$opts->{'i'}/;
	}
	$opts->{'o'} ||= 'up';
	$usort = 1 if $opts->{'o'} =~ /u/;  # enable Unicode sort
	$psort = 1 if $opts->{'o'} =~ /p/;  # enable Postname sort
			# gid sort is always last resort
    if ($opts->{'b'} && $#ARGV >= 2)
    {
        $specialfh = IO::File->new($ARGV[2], "w") || die "Can't open $ARGV[2] for writing";
        print $specialfh "<?xml version='1.0'?>\n<octaboxes>\n";
    }
}

sub extra_points
{
    my ($class, $font, $gid, $glyph) = @_;
    my (@res);

    while (my ($k, $p) = each %{$xml_dat[$gid]{'points'}})
    {
        next unless (defined $p->{'loc'});
        push (@res, [$p->{'loc'}[0], $p->{'loc'}[1], $k]);
    }
    return [@res];
}

sub outlinebox
{
    my ($xmin, $ymin, $xmax, $ymax, $amin, $smin, $amax, $smax, $col, $label) = @_;
    my ($x1, $x2, $y1, $y2);
    my (@res);
    $col = [.92, .37, .17] unless (defined $col);

    if (defined $specialfh && $label)
    { printf $specialfh "    <%s xmin='%d' xmax='%d' ymin='%d' ymax='%d' smin='%d' smax='%d' dmin='%d' dmax='%d'/>\n",
                $label, $xmin, $xmax, $ymin, $ymax, $amin, $amax, $smin, $smax; }

    $x1 = $ymax + $smin; $x2 = $amax - $ymax;
    if ($x2 < $x1) { push (@res, ['m', 0.5 * ($amax + $smin), 0.5 * ($amax - $smin)]); }
    else { push (@res, ['m', $x1, $ymax], ["l", $x2, $ymax]); }
    $y1 = $amax - $xmax; $y2 = $xmax - $smax;
    if ($y1 < $y2) { push (@res, ["l", 0.5 * ($amax + $smax), 0.5 * ($amax - $smax)]); }
    else { push (@res, ["l", $xmax, $y1], ["l", $xmax, $y2]); }
    $x2 = $amin - $ymin; $x1 = $ymin + $smax;
    if ($x1 < $x2) { push (@res, ["l", 0.5 * ($amin + $smax), 0.5 * ($amin - $smax)]); }
    else { push (@res, ["l", $x1, $ymin], ["l", $x2, $ymin]); }
    $y2 = $xmin - $smin; $y1 = $amin - $xmin;
    if ($y2 < $y1) { push (@res, ["l", 0.5 * ($amin + $smin), 0.5 * ($amin - $smin)]); }
    else { push (@res, ["l", $xmin, $y1], ["l", $xmin, $y2]); }
    push (@res, ["RG", @$col], ["s"]);
    return @res;
}

sub normalpt
{
    my ($g, $x, $y) = @_;
    my ($newx) = ($x - $g->{'xMin'}) / ($g->{'xMax'} - $g->{'xMin'});
    my ($newy) = ($y - $g->{'yMin'}) / ($g->{'yMax'} - $g->{'yMin'});
    return ($newx, $newy);
}

sub unnormalpt
{
    my ($g, $x, $y) = @_;
    my ($newx) = $x * $g->{'xMax'} + (1 - $x) * $g->{'xMin'};
    my ($newy) = $y * $g->{'yMax'} + (1 - $y) * $g->{'yMin'};
    return ($newx, $newy);
}

sub unnormald
{
    my ($g, $a, $s) = @_;
    my ($newx) = ($a + $s) * 0.5 * ($g->{'xMax'} - $g->{'xMin'}) + $g->{'xMin'};
    my ($newy) = ($a - $s) * 0.5 * ($g->{'yMax'} - $g->{'yMin'}) + $g->{'yMin'}; 
    return ($newx + $newy, $newx - $newy);
}

sub addpoint
{
    my ($g, $x, $y, $glyph) = @_;

    push (@{$g->{'points'}}, [$x, $y]);
    my ($s) = $x - $y;
    my ($a) = $x + $y;
    # minmaxes
    $g->{'bbox'}[0] = $x if (!defined $g->{'bbox'}[0] || $x < $g->{'bbox'}[0]);
    $g->{'bbox'}[1] = $x if (!defined $g->{'bbox'}[1] || $x > $g->{'bbox'}[1]);
    $g->{'bbox'}[2] = $y if (!defined $g->{'bbox'}[2] || $y < $g->{'bbox'}[2]);
    $g->{'bbox'}[3] = $y if (!defined $g->{'bbox'}[3] || $y > $g->{'bbox'}[3]);
    $g->{'dbox'}[0] = $a if (!defined $g->{'dbox'}[0] || $a < $g->{'dbox'}[0]);
    $g->{'dbox'}[1] = $a if (!defined $g->{'dbox'}[1] || $a > $g->{'dbox'}[1]);
    $g->{'dbox'}[2] = $s if (!defined $g->{'dbox'}[2] || $s < $g->{'dbox'}[2]);
    $g->{'dbox'}[3] = $s if (!defined $g->{'dbox'}[3] || $s > $g->{'dbox'}[3]);
    if ($glyph)
    {
        $glyph->{'dbox'}[0] = $a if (!defined $glyph->{'dbox'}[0] || $a < $glyph->{'dbox'}[0]);
        $glyph->{'dbox'}[1] = $a if (!defined $glyph->{'dbox'}[1] || $a > $glyph->{'dbox'}[1]);
        $glyph->{'dbox'}[2] = $s if (!defined $glyph->{'dbox'}[2] || $s < $glyph->{'dbox'}[2]);
        $glyph->{'dbox'}[3] = $s if (!defined $glyph->{'dbox'}[3] || $s > $glyph->{'dbox'}[3]);
    }
}

sub addminmax
{
    my ($e, $v) = @_;
    $e->[0] = $v if (!defined $e->[0] || $v < $e->[0]);
    $e->[1] = $v if (!defined $e->[1] || $v > $e->[1]);
}

sub overlay
{
    my ($class, $cid, $gid, $glyph, $uid, $font) = @_;
    my (@res, @grid, $i, $j, $ei, $oldx, $oldy, $oldgx, $oldgy);
    return () unless ($opts->{'b'});

    if ($specialfh)
    { printf $specialfh "  <glyph gid='%d' uid='%04X' name='%s'>\n", $gid, $uid, $font->{'post'}{'VAL'}[$gid]; }

    foreach $i (0 .. 3)
    {
        foreach $j (0 .. 3)
        {
            $grid[$i][$j] = {'cutsi' => [[], [], [], []], 'cutso' => [[], [], [], []]};
        }
    }

    $glyph->get_points();
    $ei = 0;
    for ($i = 0; $i < @{$glyph->{'x'}}; $i++)
    {
        my ($x, $y) = ($glyph->{'x'}[$i], $glyph->{'y'}[$i]);
        my ($x1, $y1) = normalpt($glyph, $x, $y);
        my ($gx, $gy) = (int($x1 * 4 - .001), int($y1 * 4 - .001));
        addpoint($grid[$gx][$gy], $x, $y, $glyph);
        if (defined $oldgx)
        {
            # handle curve lines crossing grid lines
            while ($oldgx < $gx)
            {
                my ($nbx) = ($oldgx + 1) / 4.;
                my ($t) = ($nbx - $oldx) / ($x1 - $oldx);
                my ($nby) = $t * $y1 + (1 - $t) * $oldy;
                my ($bgy) = int($nby * 4 - .001);
                my ($bx, $by) = unnormalpt($glyph, $nbx, $nby);
                addpoint($grid[$oldgx][$bgy], $bx, $by);
                addminmax($grid[$oldgx][$bgy]{'cutso'}[1], $by);
                ++$oldgx;
                addpoint($grid[$oldgx][$bgy], $bx, $by);
                addminmax($grid[$oldgx][$bgy]{'cutsi'}[0], $by);
            }
            while ($oldgx > $gx)
            {
                my ($nbx) = $oldgx / 4.;
                my ($t) = ($nbx - $oldx) / ($x1 - $oldx);
                my ($nby) = $t * $y1 + (1 - $t) * $oldy;
                my ($bgy) = int($nby * 4 - .001);
                my ($bx, $by) = unnormalpt($glyph, $nbx, $nby);
                addpoint($grid[$oldgx][$bgy], $bx, $by);
                addminmax($grid[$oldgx][$bgy]{'cutso'}[0], $by);
                --$oldgx;
                addpoint($grid[$oldgx][$bgy], $bx, $by);
                addminmax($grid[$oldgx][$bgy]{'cutsi'}[1], $by);
            }
            while ($oldgy < $gy)
            {
                my ($nby) = ($oldgy + 1) / 4.;
                my ($t) = ($nby - $oldy) / ($y1 - $oldy);
                my ($nbx) = $t * $x1 + (1 - $t) * $oldx;
                my ($bgx) = int($nbx * 4 - .001);
                my ($bx, $by) = unnormalpt($glyph, $nbx, $nby);
                addpoint($grid[$bgx][$oldgy], $bx, $by);
                addminmax($grid[$bgx][$oldgy]{'cutso'}[3], $bx);
                ++$oldgy;
                addpoint($grid[$bgx][$oldgy], $bx, $by);
                addminmax($grid[$bgx][$oldgy]{'cutsi'}[2], $bx);
            }
            while ($oldgy > $gy)
            {
                my ($nby) = $oldgy / 4.;
                my ($t) = ($nby - $oldy) / ($y1 - $oldy);
                my ($nbx) = $t * $x1 + (1 - $t) * $oldx;
                my ($bgx) = int($nbx * 4 - .001);
                my ($bx, $by) = unnormalpt($glyph, $nbx, $nby);
                addpoint($grid[$bgx][$oldgy], $bx, $by);
                addminmax($grid[$bgx][$oldgy]{'cutso'}[2], $bx);
                --$oldgy;
                addpoint($grid[$bgx][$oldgy], $bx, $by);
                addminmax($grid[$bgx][$oldgy]{'cutsi'}[3], $bx);
            }
        }
        if ($glyph->{'endPoints'}[$ei] == $i)
        {
            ++$ei;
            $oldgx = undef;
            $oldgy = undef;
        }
        else
        {
            $oldx = $x1;
            $oldy = $y1;
            $oldgx = $gx;
            $oldgy = $gy;
        }
    }

    push (@res, outlinebox($glyph->{'xMin'}, $glyph->{'yMin'}, $glyph->{'xMax'}, $glyph->{'yMax'},
                          $glyph->{'dbox'}[0], $glyph->{'dbox'}[2], $glyph->{'dbox'}[1], $glyph->{'dbox'}[3], [.375, .754, .078], 'glyphbox'));
    # add extra corner points if the corner is within a stem
    foreach $i (0 .. 3)
    {
        foreach $j (0 .. 3)
        {
            my ($g) = $grid[$j][$i];

            if (defined $g->{'cutsi'}[0][0] && (!defined $g->{'cutso'}[0][0] || $g->{'cutsi'}[0][0] < $g->{'cutso'}[0][0])
                || defined $g->{'cutso'}[2][0] && (!defined $g->{'cutsi'}[2][0] || $g->{'cutso'}[2][0] < $g->{'cutsi'}[2][0]))
            { addpoint($g, unnormalpt($glyph, $j / 4, $i / 4)); }
            if (defined $g->{'cutso'}[0][1] && (!defined $g->{'cutsi'}[0][1] || $g->{'cutso'}[0][1] > $g->{'cutsi'}[0][1])
                || defined $g->{'cutsi'}[3][0] && (!defined $g->{'cutso'}[3][0] || $g->{'cutsi'}[3][0] < $g->{'cutso'}[3][0]))
            { addpoint($g, unnormalpt($glyph, $j / 4, ($i + 1) / 4)); }
            if (defined $g->{'cutso'}[1][0] && (!defined $g->{'cutsi'}[1][0] || $g->{'cutso'}[1][0] < $g->{'cutsi'}[1][0])
                || defined $g->{'cutsi'}[2][1] && (!defined $g->{'cutso'}[2][1] || $g->{'cutsi'}[2][1] > $g->{'cutso'}[2][1]))
            { addpoint($g, unnormalpt($glyph, ($j + 1) / 4, $i / 4)); }
            if (defined $g->{'cutsi'}[1][1] && (!defined $g->{'cutso'}[1][1] || $g->{'cutsi'}[1][1] > $g->{'cutso'}[1][1])
                || defined $g->{'cutso'}[3][1] && (!defined $g->{'cutsi'}[3][1] || $g->{'cutso'}[3][1] > $g->{'cutsi'}[3][1]))
            { addpoint($g, unnormalpt($glyph, ($j + 1) / 4, ($i + 1) / 4)); }

            # create box outlines
            next if (!defined $g->{'bbox'}[0]);     # no points in this box so no outline
#            push (@res, outlinebox( unnormalpt($glyph, $g->{'bbox'}[0], $g->{'bbox'}[2]),
#                                    unnormalpt($glyph, $g->{'bbox'}[1], $g->{'bbox'}[3]),
#                                    unnormald($glyph, $g->{'dbox'}[0], $g->{'dbox'}[2]),
#                                    unnormald($glyph, $g->{'dbox'}[1], $g->{'dbox'}[3])));
            push (@res, outlinebox( $g->{'bbox'}[0], $g->{'bbox'}[2], $g->{'bbox'}[1], $g->{'bbox'}[3],
                                    $g->{'dbox'}[0], $g->{'dbox'}[2], $g->{'dbox'}[1], $g->{'dbox'}[3], undef, "subbox grid='$j,$i'"));
        }
    }
    if ($specialfh)
    { print $specialfh "  </glyph>\n"; }
    return @res;
}

sub final
{
    if ($specialfh)
    {
        print $specialfh "</octaboxes>\n";
        $specialfh->close();
    }
}

__END__

=head1 NAME

fret - Font REporting Tool

=head1 SYNOPSIS

  fret [-p package] [-a ap.xml] [-f] [-g] [-i re] [-r] [-s size] [-q]
            font_file [out_file] [bounding_file]

Generates a report on a font according to a particular package. In some
contexts the package may be over-ridden. Paper size may also be specified.

=head1 OPTIONS

If no out_file is given then out_file becomes font_file.pdf (removing .ttf
if present)

  -a ap.xml     Attachment point database file
  -b            Draw bounding octogon. Outputs bounding file if specified.
  -f            Don't try to save memory on large fonts (>1000 glyphs)
  -g            Add one glyph per page report following summary report
  -h            Mode for glyph per page output. Bitfield:
                1 = bit 0       don't output curve point positions
                2 = bit 0       don't output attachment point positions
  -i re         Regular expression that identifies what attachment points
                to include in the report
  -m points     Sets glyph size in the box regardless of what is calculated
                Regardless of the consequences for clashes
  -o sort       Define sort order. Can be combinations of u (Unicode),
                p (postname), or i (glyph index). When supplied, priority
                is u, then p, then i. Default upi.
  -p package    Perl package specification to use for report information
                (must be first option)
  -q            quiet mode
  -r            Don't output report lines, fill the page with glyph boxes
  -s size       paper size: a4, ltr, legal
  -?            longer help

=head1 DESCRIPTION

FRET creates a PDF report from a TrueType font containing information about
every glyph in the font. It sorts the glyphs by Unicode identifier and then
for those glyphs with no Unicode identifier it sorts them by glyph name and
then by glyph number.

In addition it is possible to get a report including a page per glyph with
a large outline of each glyph perhaps with the drawn points.

If the optional ap.xml file is specified on the command line then attachment
point information is included in the report. See ttfbuilder for details of
the ap.xml file format. Only attachment points that match the regular
expression supplied via the -i option will be included.

If supplied, -p must be the first option.

=head1 SEE ALSO

ttfbuilder

=head1 AUTHOR

Martin Hosken L<http://scripts.sil.org/FontUtils>.
(see CONTRIBUTORS for other authors).

=head1 LICENSING

Copyright (c) 1998-2014, SIL International (http://www.sil.org)

This script is released under the terms of the Artistic License 2.0.
For details, see the full text of the license in the file LICENSE.

=cut
