source: git/lib/afm2txf.pl @ e9bd0467

RELEASE/1.2debug-cidebug-ci-sanitisersstereowalls-data
Last change on this file since e9bd0467 was e9bd0467, checked in by Olly Betts <olly@…>, 13 years ago

lib/afm2txf.pl: Put the digits first in the font texture to help
ensure they are all on the same line and will exactly align
vertically when rendered - a slight discrepancy here is particularly
visible in the colour key legends and compass bearing.

git-svn-id: file:///home/survex-svn/survex/trunk@3796 4b37db11-9a0c-4f06-9ece-9ab7cdaee568

  • Property mode set to 100755
File size: 11.0 KB
Line 
1#!/usr/bin/perl -w
2
3# afm2txf.pl 0.2
4#
5# Generates .txf font textures from Type 1 fonts
6# Requirements: Ghostscript, ImageMagick
7#
8# Usage:
9#       afm2txf.pl [-o OUTPUT.txf] whatever.afm
10#
11# Changelog:
12#       0.2 (06/28/2002): Generate fonts with proper padding
13#       0.1 (06/28/2002): Initial version
14#
15# Copyright (C) 2002 Andrew James Ross
16# Copyright (C) 2010,2011 Olly Betts
17#
18# This program is free software; you can redistribute it and/or modify
19# it under the terms of the GNU General Public License version 2 as
20# published by the Free Software Foundation.
21
22use strict;
23
24my $output;
25if (scalar @ARGV >= 1) {
26    my $arg = $ARGV[0];
27    if ($arg =~ s/^-o//) {
28        shift;
29        if ($arg eq '') {
30            if (scalar @ARGV == 0) {
31                die "-o needs an argument\n";
32            }
33            $output = shift;
34        } else {
35            $output = $arg;
36        }
37    }
38}
39
40my $METRICS = shift or die; # AFM file
41
42# Texture size
43my $TEXSIZ = 256;
44
45# Padding around each character, for mipmap separation
46my $PADDING = 4;
47
48# Antialiasing multiplier.  Should be 16 for production work.  As low
49# as 4 works well for testing.
50my $DOWNSAMPLE = 16;
51
52# The printable ISO-8859-1 characters (and space and hard space) and their
53# postscript glyph names.  We use names because not all postscript
54# fonts are encoded using ASCII.  AFM metrics generated by ttf2afm, in
55# fact, don't have any numerical character IDs at all.  In principle,
56# this mechanism will work for any 8 bit font encoding, you just have
57# to do the legwork of figuring out the name to ID mapping.
58my %REVCHARS = (32=>'space', 33=>'exclam', 34=>'quotedbl',
59             35=>'numbersign', 36=>'dollar', 37=>'percent',
60             38=>'ampersand', 39=>'quotesingle', 40=>'parenleft',
61             41=>'parenright', 42=>'asterisk', 43=>'plus',
62             44=>'comma', 45=>'hyphen', 46=>'period', 47=>'slash',
63             48=>'zero', 49=>'one', 50=>'two', 51=>'three',
64             52=>'four', 53=>'five', 54=>'six', 55=>'seven',
65             56=>'eight', 57=>'nine', 58=>'colon', 59=>'semicolon',
66             60=>'less', 61=>'equal', 62=>'greater', 63=>'question',
67             64=>'at', 65=>'A', 66=>'B', 67=>'C', 68=>'D', 69=>'E',
68             70=>'F', 71=>'G', 72=>'H', 73=>'I', 74=>'J', 75=>'K',
69             76=>'L', 77=>'M', 78=>'N', 79=>'O', 80=>'P', 81=>'Q',
70             82=>'R', 83=>'S', 84=>'T', 85=>'U', 86=>'V', 87=>'W',
71             88=>'X', 89=>'Y', 90=>'Z', 91=>'bracketleft',
72             92=>'backslash', 93=>'bracketright', 94=>'asciicircum',
73             95=>'underscore', 96=>'quoteleft', 97=>'a', 98=>'b', 99=>'c',
74             100=>'d', 101=>'e', 102=>'f', 103=>'g', 104=>'h',
75             105=>'i', 106=>'j', 107=>'k', 108=>'l', 109=>'m',
76             110=>'n', 111=>'o', 112=>'p', 113=>'q', 114=>'r',
77             115=>'s', 116=>'t', 117=>'u', 118=>'v', 119=>'w',
78             120=>'x', 121=>'y', 122=>'z', 123=>'braceleft',
79             124=>'bar', 125=>'braceright', 126=>'asciitilde',
80             160=>'space', 161=>'exclamdown', 162=>'cent', 163=>'sterling',
81             164=>'currency', 165=>'yen', 166=>'brokenbar', 167=>'section',
82             168=>'dieresis', 169=>'copyright', 170=>'ordfeminine', 171=>'guillemotleft',
83             172=>'logicalnot', 173=>'hyphen', 174=>'registered', 175=>'macron',
84             176=>'degree', 177=>'plusminus', 178=>'twosuperior', 179=>'threesuperior',
85             180=>'acute', 181=>'mu', 182=>'paragraph', 183=>'bullet',
86             184=>'cedilla', 185=>'onesuperior', 186=>'ordmasculine', 187=>'guillemotright',
87             188=>'onequarter', 189=>'onehalf', 190=>'threequarters', 191=>'questiondown',
88             192=>'Agrave', 193=>'Aacute', 194=>'Acircumflex', 195=>'Atilde',
89             196=>'Adieresis', 197=>'Aring', 198=>'AE', 199=>'Ccedilla',
90             200=>'Egrave', 201=>'Eacute', 202=>'Ecircumflex', 203=>'Edieresis',
91             204=>'Igrave', 205=>'Iacute', 206=>'Icircumflex', 207=>'Idieresis',
92             208=>'Eth', 209=>'Ntilde', 210=>'Ograve', 211=>'Oacute',
93             212=>'Ocircumflex', 213=>'Otilde', 214=>'Odieresis', 215=>'multiply',
94             216=>'Oslash', 217=>'Ugrave', 218=>'Uacute', 219=>'Ucircumflex',
95             220=>'Udieresis', 221=>'Yacute', 222=>'Thorn', 223=>'germandbls',
96             224=>'agrave', 225=>'aacute', 226=>'acircumflex', 227=>'atilde',
97             228=>'adieresis', 229=>'aring', 230=>'ae', 231=>'ccedilla',
98             232=>'egrave', 233=>'eacute', 234=>'ecircumflex', 235=>'edieresis',
99             236=>'igrave', 237=>'iacute', 238=>'icircumflex', 239=>'idieresis',
100             240=>'eth', 241=>'ntilde', 242=>'ograve', 243=>'oacute',
101             244=>'ocircumflex', 245=>'otilde', 246=>'odieresis', 247=>'divide',
102             248=>'oslash', 249=>'ugrave', 250=>'uacute', 251=>'ucircumflex',
103             252=>'udieresis', 253=>'yacute', 254=>'thorn', 255=>'ydieresis'
104             );
105
106my %CHARS;
107while (my ($k, $v) = each %REVCHARS) {
108    $CHARS{$v} = $k unless exists $CHARS{$v};
109}
110
111my %metrics = ();
112my %positions = ();
113
114#
115# Parse the font metrics.  This is a 5 element array.  All numbers are
116# expressed as a fraction of the line spacing.
117# 0:    nominal width (distance to the next character)
118# 1, 2: Coordinates of the lower left corner of the bounding box,
119#       relative to the nominal position.
120# 3, 4: Size of the bounding box
121#
122print STDERR "Reading font metrics...\n";
123my $FONT;
124open METRICS, '<', $METRICS or die $!;
125my $m;
126while (defined($m = <METRICS>)) {
127    if ($m =~ /^FontName (\S*)/) { $FONT = $1; next; }
128    next unless $m =~ /^C /;
129    chomp $m;
130
131    die "No name: $m" if $m !~ /N\s+([^\s]+)\s+;/;
132    my $name = $1;
133
134    die "No box: $m"
135        if $m !~ /B\s+([-0-9]+)\s+([-0-9]+)\s+([-0-9]+)\s+([-0-9]+)\s+;/;
136    my ($left, $bottom, $right, $top) = ($1/1000, $2/1000, $3/1000, $4/1000);
137
138    die "No width: $m" if $m !~ /WX\s+([-0-9]+)/;
139    my $nomwid = $1/1000; # nominal, not physical width!
140
141    # The coordinates of the corner relative to the character
142    # "position"
143    my ($x, $y) = (-$left, -$bottom);
144    my ($w, $h) = ($right-$left, $top-$bottom);
145
146    $metrics{$name} = [$nomwid, $x, $y, $w, $h];
147}
148close METRICS;
149
150die "No FontName found in metrics" if not defined $FONT;
151
152# Sanitise $FONT.
153$FONT =~ s!/!_!g;
154
155#
156# Find the height of the tallest character, and print some warnings
157#
158my $maxhgt = 0;
159foreach my $c (keys %CHARS) {
160    if(!defined $metrics{$c}) {
161        print STDERR "% WARNING: no metrics for char $c.  Skipping.\n";
162        next;
163    }
164    if($metrics{$c}->[4] > $maxhgt) { $maxhgt = $metrics{$c}->[4]; }
165}
166if($maxhgt == 0) {
167    print STDERR "No usable characters found.  Bailing out.\n";
168    exit 1;
169}
170
171#
172# Do the layout.  Keep increasing the row count until the characters
173# just fit.  This isn't terribly elegant, but it's simple.
174#
175print STDERR "Laying out";
176my $rows = 1;
177my $PS;
178my $LINEHGT;
179while(!defined ($PS = genPostscript($rows))) { $rows++; }
180print STDERR " ($rows rows)\n";
181
182#
183# Call ghostscript to render
184#
185print STDERR "Rendering Postscript...\n";
186my $res = $TEXSIZ * $DOWNSAMPLE;
187my $pid = open PS, "|gs -r$res -g${res}x${res} -sDEVICE=ppm -sOutputFile=\Q$FONT\E.ppm > /dev/null";
188die "Couldn't spawn ghostscript interpreter" if !defined $pid;
189foreach (@$PS) {
190    print PS "$_\n";
191}
192close PS;
193waitpid($pid, 0);
194
195#
196# Downsample with ImageMagick
197#
198print STDERR "Antialiasing image...\n";
199system("mogrify -geometry ${TEXSIZ}x${TEXSIZ} \Q$FONT\E.ppm") == 0
200    or die "Couldn't rescale $FONT.ppm";
201
202#
203# Generate the .txf file
204#
205print STDERR "Generating textured font file...\n";
206
207# Prune undefined glyphs
208foreach my $c (keys %metrics) {
209    delete $metrics{$c} if !defined $CHARS{$c};
210}
211
212sub round { sprintf "%.0f", $_[0] }
213$output = "$FONT.txf" unless defined $output;
214open TXF, '>', $output or die;
215print TXF pack "V", 0x667874ff;
216print TXF pack "V", 0x12345678;
217print TXF pack "V", 0;
218print TXF pack "V", $TEXSIZ;
219print TXF pack "V", $TEXSIZ;
220print TXF pack "V", round($TEXSIZ * $LINEHGT);
221print TXF pack "V", 0;
222my @chars = sort keys %REVCHARS;
223print TXF pack "V", scalar @chars;
224foreach my $c (@chars) {
225    my $name = $REVCHARS{$c};
226    my $m = $metrics{$name};
227    my $p = $positions{$name};
228    my $step = round($m->[0] * $LINEHGT * $TEXSIZ);
229
230    # Pad the bounding box, to handle areas that outside.  This can
231    # happen due to thick lines in the font path, or be an artifact of
232    # the downsampling.
233    my ($w, $h, $xoff, $yoff, $x, $y);
234    if ($name eq 'space') {
235        $w = $h = $xoff = $yoff = $x = $y = 0;
236    } else {
237        $w = round($m->[3] * $LINEHGT * $TEXSIZ + 2*$PADDING);
238        $h = round($m->[4] * $LINEHGT * $TEXSIZ + 2*$PADDING);
239        $xoff = -round($m->[1] * $LINEHGT * $TEXSIZ) - $PADDING;
240        $yoff = -round($m->[2] * $LINEHGT * $TEXSIZ) - $PADDING;
241        $x = round($p->[0] * $TEXSIZ - $PADDING);
242        $y = round($p->[1] * $TEXSIZ - $PADDING);
243    }
244    print TXF pack "v", $c;
245    print TXF pack "C", $w;
246    print TXF pack "C", $h;
247    print TXF pack "c", $xoff;
248    print TXF pack "c", $yoff;
249    print TXF pack "C", $step;
250    print TXF pack "C", 0;
251    print TXF pack "v", $x;
252    print TXF pack "v", $y;
253}
254
255# Read in the .ppm file, dump the duplicate color values (ghostscript
256# won't generate pgm's) and write to the end of the .txf.  Remember to
257# swap the order of the rows; OpenGL textures are bottom-up.
258open PPM, "$FONT.ppm" or die;
259my $pixel;
260foreach my $r (1 .. $TEXSIZ) {
261    seek PPM, -3*$r*$TEXSIZ, 2 or die;
262    foreach (1 .. $TEXSIZ) {
263        read PPM, $pixel, 3 or die;
264        print TXF substr($pixel, 0, 1);
265    }
266}
267close PPM;
268close TXF;
269
270# Clean up
271unlink("$FONT.ppm");
272
273########################################################################
274########################################################################
275########################################################################
276
277# Put the digits first to help ensure they are all on the same line in the
278# texture and will exactly align vertically when rendered - a slight
279# discrepancy here is particularly visible in the colour key legends and
280# compass bearing.
281sub render_order {
282    my $a = $CHARS{$a};
283    my $b = $CHARS{$b};
284    my $a_dig = chr($a) =~ /\d/;
285    my $b_dig = chr($b) =~ /\d/;
286    if ($a_dig ^ $b_dig) {
287        return $a_dig ? -1 : 1;
288    }
289    return $a <=> $b;
290}
291
292sub genPostscript {
293    my $rows = shift;
294    my $rowhgt = 1/$rows;
295
296    my @PS = ();
297
298    # The canonical "point size" number, in texture space
299    $LINEHGT = ($rowhgt - 2*$PADDING/$TEXSIZ) / $maxhgt;
300
301    # Get to where we want.  Draw the whole thing in a 1 inch square at
302    # the bottom left of the "page".
303    push @PS, "72 72 scale";
304
305    # Fill the square with black
306    push @PS, "0 setgray";
307    push @PS, "-1 -1 moveto";
308    push @PS, "-1 1 lineto 1 1 lineto 1 -1 lineto";
309    push @PS, "closepath";
310    push @PS, "fill";
311
312    # Draw in white
313    push @PS, "1 setgray";
314
315    # Generate our PUSH @PS, font
316    push @PS, "/$FONT findfont $LINEHGT scalefont setfont";
317
318    my $x = $PADDING/$TEXSIZ;
319    my $y = 1 - $rowhgt + $PADDING/$TEXSIZ;
320    my @chars = sort render_order (keys %CHARS);
321    foreach my $c (@chars) {
322        next if $c eq 'space';
323        my $m = $metrics{$c};
324        next if !defined $m;
325
326        # No space?
327        my $w = $m->[3]*$LINEHGT;
328        if($x + $w + $PADDING/$TEXSIZ > 1) {
329            $x = $PADDING/$TEXSIZ;
330            $y -= $rowhgt;
331            return undef if $y < 0;
332        }
333
334        # Record where in the texture the box ended up
335        $positions{$c} = [$x, $y];
336
337        my $vx = $x + $m->[1]*$LINEHGT;
338        my $vy = $y + $m->[2]*$LINEHGT;
339
340        push @PS, "$vx $vy moveto";
341        push @PS, "/$c glyphshow";
342
343        # Next box...
344        $x += $w + 2*$PADDING/$TEXSIZ;
345    }
346
347    push @PS, "showpage";
348
349    return \@PS;
350}
Note: See TracBrowser for help on using the repository browser.