source: git/lib/afm2txf.pl @ bcb68d3

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

lib/afm2txf.pl: Add a kludge to prefer to round values up when
generating the .txf file which empirically results in better
horizontal spacing (this should be fixed properly, but this is
at least an easy improvement for now).

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

  • Property mode set to 100755
File size: 11.2 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
212#sub round { $_[0] > 0 ? int($_[0] + 0.5) : int($_[0] - 0.5) }
213# Bias rounding of positives numbers to be upwards for better spacing.
214# FIXME: sort out the spacing properly - this is just a kludge.
215sub round { $_[0] > 0 ? int($_[0] + 0.9) : int($_[0] - 0.5) }
216$output = "$FONT.txf" unless defined $output;
217open TXF, '>', $output or die;
218print TXF pack "V", 0x667874ff;
219print TXF pack "V", 0x12345678;
220print TXF pack "V", 0;
221print TXF pack "V", $TEXSIZ;
222print TXF pack "V", $TEXSIZ;
223print TXF pack "V", round($TEXSIZ * $LINEHGT);
224print TXF pack "V", 0;
225my @chars = sort keys %REVCHARS;
226print TXF pack "V", scalar @chars;
227foreach my $c (@chars) {
228    my $name = $REVCHARS{$c};
229    my $m = $metrics{$name};
230    my $p = $positions{$name};
231    my $step = round($m->[0] * $LINEHGT * $TEXSIZ);
232
233    # Pad the bounding box, to handle areas that outside.  This can
234    # happen due to thick lines in the font path, or be an artifact of
235    # the downsampling.
236    my ($w, $h, $xoff, $yoff, $x, $y);
237    if ($m->[3] == 0 && $m->[4] == 0) {
238        $w = $h = $xoff = $yoff = $x = $y = 0;
239    } else {
240        $w = round($m->[3] * $LINEHGT * $TEXSIZ + 2*$PADDING);
241        $h = round($m->[4] * $LINEHGT * $TEXSIZ + 2*$PADDING);
242        $xoff = -round($m->[1] * $LINEHGT * $TEXSIZ) - $PADDING;
243        $yoff = -round($m->[2] * $LINEHGT * $TEXSIZ) - $PADDING;
244        $x = round($p->[0] * $TEXSIZ - $PADDING);
245        $y = round($p->[1] * $TEXSIZ - $PADDING);
246    }
247    print TXF pack "v", $c;
248    print TXF pack "C", $w;
249    print TXF pack "C", $h;
250    print TXF pack "c", $xoff;
251    print TXF pack "c", $yoff;
252    print TXF pack "C", $step;
253    print TXF pack "C", 0;
254    print TXF pack "v", $x;
255    print TXF pack "v", $y;
256}
257
258# Read in the .ppm file, dump the duplicate color values (ghostscript
259# won't generate pgm's) and write to the end of the .txf.  Remember to
260# swap the order of the rows; OpenGL textures are bottom-up.
261open PPM, "$FONT.ppm" or die;
262my $pixel;
263foreach my $r (1 .. $TEXSIZ) {
264    seek PPM, -3*$r*$TEXSIZ, 2 or die;
265    foreach (1 .. $TEXSIZ) {
266        read PPM, $pixel, 3 or die;
267        print TXF substr($pixel, 0, 1);
268    }
269}
270close PPM;
271close TXF;
272
273# Clean up
274unlink("$FONT.ppm");
275
276########################################################################
277########################################################################
278########################################################################
279
280# Put the digits first to help ensure they are all on the same line in the
281# texture and will exactly align vertically when rendered - a slight
282# discrepancy here is particularly visible in the colour key legends and
283# compass bearing.
284sub render_order {
285    my $a = $CHARS{$a};
286    my $b = $CHARS{$b};
287    my $a_dig = chr($a) =~ /\d/;
288    my $b_dig = chr($b) =~ /\d/;
289    if ($a_dig ^ $b_dig) {
290        return $a_dig ? -1 : 1;
291    }
292    return $a <=> $b;
293}
294
295sub genPostscript {
296    my $rows = shift;
297    my $rowhgt = 1/$rows;
298
299    my @PS = ();
300
301    # The canonical "point size" number, in texture space
302    $LINEHGT = ($rowhgt - 2*$PADDING/$TEXSIZ) / $maxhgt;
303
304    # Get to where we want.  Draw the whole thing in a 1 inch square at
305    # the bottom left of the "page".
306    push @PS, "72 72 scale";
307
308    # Fill the square with black
309    push @PS, "0 setgray";
310    push @PS, "-1 -1 moveto";
311    push @PS, "-1 1 lineto 1 1 lineto 1 -1 lineto";
312    push @PS, "closepath";
313    push @PS, "fill";
314
315    # Draw in white
316    push @PS, "1 setgray";
317
318    # Generate our PUSH @PS, font
319    push @PS, "/$FONT findfont $LINEHGT scalefont setfont";
320
321    my $x = $PADDING/$TEXSIZ;
322    my $y = 1 - $rowhgt + $PADDING/$TEXSIZ;
323    my @chars = sort render_order (keys %CHARS);
324    foreach my $c (@chars) {
325        next if $c eq 'space';
326        my $m = $metrics{$c};
327        next if !defined $m;
328
329        # No space?
330        my $w = $m->[3]*$LINEHGT;
331        if($x + $w + $PADDING/$TEXSIZ > 1) {
332            $x = $PADDING/$TEXSIZ;
333            $y -= $rowhgt;
334            return undef if $y < 0;
335        }
336
337        # Record where in the texture the box ended up
338        $positions{$c} = [$x, $y];
339
340        my $vx = $x + $m->[1]*$LINEHGT;
341        my $vy = $y + $m->[2]*$LINEHGT;
342
343        push @PS, "$vx $vy moveto";
344        push @PS, "/$c glyphshow";
345
346        # Next box...
347        $x += $w + 2*$PADDING/$TEXSIZ;
348    }
349
350    push @PS, "showpage";
351
352    return \@PS;
353}
Note: See TracBrowser for help on using the repository browser.