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