On Tue, Jul 25, 2006 at 02:11:34PM +0100, Simon Kelley wrote: > Attached is a new version of > /usr/share/perl5/SlimServer/Slim/Display/Graphics.pm which incorporates > your patch, and some more stuff which gives access to GD's built-in > fixed width fonts.
Great, thanks! > The results on a Squeezebox 2/3 are subjectively not as good as TTF > vera, but on a SB1 this might be a better solution, at least for small > fonts. Please could you try and find a set of good parameters (font, > width and base) that looks reasonably nice? I don't have SB1 to > experiment with. OK, please find attached a new Graphics.pm and a patch (if that is more convenient). Firstly I added a bit of code to work out how wide each fixed font glyph is and I changed the GDWidth parameter into a GDFixedspacing parameter which is the number of pixels gap between each glyph. This makes the fonts pseudo-proportional and hence a lot nicer. I made a guess at what GDFixedspacing should be for v2/3 (I only have v1) so you'll need to have a quick check! I found a good set of parameters for the fonts for v1 I think. The tiny font is 7 pixels high, whereas the small font is 11. So tiny is the only font that can have two lines worth of text on a 16 pixel high screen. This means that the only difference between the first two zoom settings is the font spacing. This seems to work reasonably well though. It would be possible to use capital tiny font + small font to get a different effect. I didn't implement that though as there are still descenders in the symbols. This is a marked improvement on the scaled TTF fonts - thanks for implementing it! Cheers Nick -- Nick Craig-Wood <[EMAIL PROTECTED]> -- http://www.craig-wood.com/nick
--- Graphics.pm.simon1 2006-07-25 20:31:35.000000000 +0100 +++ Graphics.pm 2006-07-25 21:38:38.000000000 +0100 @@ -44,7 +44,7 @@ 'GDBaseline' => 9, # 'GDFixedfont' => GD::gdLargeFont, 'GDFixedbase' => 0, - 'GDWidth' => 7, + 'GDFixedspacing' => 4, 'GDHeight' => 32, }, @@ -53,7 +53,7 @@ 'GDBaseline' => 27, # 'GDFixedfont' => GD::gdLargeFont, 'GDFixedbase' => 15, - 'GDWidth' => 7, + 'GDFixedspacing' => 4, 'GDHeight' => 32, }, @@ -64,7 +64,7 @@ # 'GDFixedfont' => GD::gdSmallFont, 'GDFixedbase' => 0, 'GDHeight' => 32, - 'GDWidth' => 6, + 'GDFixedspacing' => 3, }, 'light.2' => { @@ -72,7 +72,7 @@ 'GDBaseline' => 28, # 'GDFixedfont' => GD::gdSmallFont, 'GDFixedbase' => 15, - 'GDWidth' => 6, + 'GDFixedspacing' => 3, 'GDHeight' => 32, }, @@ -82,7 +82,7 @@ 'GDBaseline' => 25, # 'GDFixedfont' => GD::gdGiantFont, 'GDFixedbase' => 9, - 'GDWidth' => 9, + 'GDFixedspacing' => 5, 'GDHeight' => 32, }, @@ -92,18 +92,26 @@ 'GDBaseline' => 7, # 'GDFixedfont' => GD::gdSmallFont, 'GDFixedbase' => 10, + 'GDFixedspacing' => 6, 'GDHeight' => 32, }, + # v1: Small size - .1 is top line, .2 is bottom. 'small.1' => { 'GDFontSize' => 5, 'GDBaseline' => 7, + 'GDFixedfont' => GD::gdTinyFont, # 7 px + 'GDFixedbase' => 0, + 'GDFixedspacing' => 2, 'GDHeight' => 16, }, 'small.2' => { 'GDFontSize' => 6, 'GDBaseline' => 14, + 'GDFixedfont' => GD::gdTinyFont, + 'GDFixedbase' => 8, + 'GDFixedspacing' => 2, 'GDHeight' => 16, }, @@ -111,12 +119,18 @@ 'medium.1' => { 'GDFontSize' => 6, 'GDBaseline' => 6, + 'GDFixedfont' => GD::gdTinyFont, + 'GDFixedbase' => 0, + 'GDFixedspacing' => 3, 'GDHeight' => 16, }, 'medium.2' => { 'GDFontSize' => 7, 'GDBaseline' => 14, + 'GDFixedfont' => GD::gdTinyFont, + 'GDFixedbase' => 8, + 'GDFixedspacing' => 3, 'GDHeight' => 16, }, @@ -124,6 +138,9 @@ 'large.2' => { 'GDFontSize' => 10, 'GDBaseline' => 13, + 'GDFixedfont' => GD::gdSmallFont, # 11 px + 'GDFixedbase' => 1, + 'GDFixedspacing' => 3, 'GDHeight' => 16, }, @@ -131,6 +148,9 @@ 'huge.2' => { 'GDFontSize' => 13, 'GDBaseline' => 13, + 'GDFixedfont' => GD::gdLargeFont, # 16 px + 'GDFixedbase' => 0, + 'GDFixedspacing' => 4, 'GDHeight' => 16, }, ); @@ -245,7 +265,7 @@ # 'sazanami-gothic' from 'waka'. # # They seem to work pretty well for CODE2000 & Cyberbit as well. - dsully - my ($GDFontSize, $GDBaseline, $GDFixedfont, $GDFixedbase, $GDWidth, $GDHeight); + my ($GDFontSize, $GDBaseline, $GDFixedfont, $GDFixedbase, $GDWidth, $GDHeight, $GDFixedspacing); my $useTTFNow = 0; my $useBuiltin = 0; @@ -256,7 +276,7 @@ $GDBaseline = $font2TTF{$fontname}->{'GDBaseline'}; $GDFixedfont = $font2TTF{$fontname}->{'GDFixedfont'}; $GDFixedbase = $font2TTF{$fontname}->{'GDFixedbase'}; - $GDWidth = $font2TTF{$fontname}->{'GDWidth'}; + $GDFixedspacing = $font2TTF{$fontname}->{'GDFixedspacing'}; $GDHeight = $font2TTF{$fontname}->{'GDHeight'}; } @@ -297,9 +317,19 @@ # in the libgd manual page at # http://www.boutell.com/gd/manual2.0.9.html#gdImageStringFT. # - + if ($useBuiltin) { $gd->string($GDFixedfont, 0, $GDFixedbase, chr($ord), $GDBlack); + # Find out how wide the glyph is + $GDWidth = 32; + WIDTH: for (my $x = $GDWidth; $x >= 0 ; $x--) { + for (my $y = 0; $y < $GDHeight; $y++) { + last WIDTH if $gd->getPixel($x,$y) == $GDBlack; + } + $GDWidth--; + } + # Add spacing + $GDWidth += $GDFixedspacing; } else { @GDbounds = $gd->stringFT(-1*$GDBlack, $TTFFontFile, $GDFontSize, 0, 0, $GDBaseline, "&#${ord};"); $GDWidth = $GDbounds[2];
package Slim::Display::Graphics; # SlimServer Copyright (c) 2001-2004 Sean Adams, Slim Devices Inc. # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License, # version 2. use strict; use File::Spec::Functions qw(catdir); use FindBin qw($Bin); use Tie::Cache::LRU; use Slim::Utils::Misc; use Slim::Utils::OSDetect; use Storable; our $fonts; our $fonthash; our $fontheight; our $fontextents; my $char0 = chr(0); my $ord0a = ord("\x0a"); # TrueType support by using GD my $canUseGD = eval { require GD; return 1; }; my $gdError = $@; my ($gd, $GDBlack, $GDWhite, $TTFFontFile, $useTTFCache, $useTTF); # Keep a cache of up to 256 characters at a time. tie my %TTFCache, 'Tie::Cache::LRU', 256; my %font2TTF = ( # The standard size - .1 is top line, .2 is bottom. 'standard.1' => { 'GDFontSize' => 8, 'GDBaseline' => 9, # 'GDFixedfont' => GD::gdLargeFont, 'GDFixedbase' => 0, 'GDFixedspacing' => 4, 'GDHeight' => 32, }, 'standard.2' => { 'GDFontSize' => 12, 'GDBaseline' => 27, # 'GDFixedfont' => GD::gdLargeFont, 'GDFixedbase' => 15, 'GDFixedspacing' => 4, 'GDHeight' => 32, }, # Small size - .1 is top line, .2 is bottom. 'light.1' => { 'GDFontSize' => 8, 'GDBaseline' => 9, # 'GDFixedfont' => GD::gdSmallFont, 'GDFixedbase' => 0, 'GDHeight' => 32, 'GDFixedspacing' => 3, }, 'light.2' => { 'GDFontSize' => 10, 'GDBaseline' => 28, # 'GDFixedfont' => GD::gdSmallFont, 'GDFixedbase' => 15, 'GDFixedspacing' => 3, 'GDHeight' => 32, }, # Huge - only one line. 'full.2' => { 'GDFontSize' => 22, 'GDBaseline' => 25, # 'GDFixedfont' => GD::gdGiantFont, 'GDFixedbase' => 9, 'GDFixedspacing' => 5, 'GDHeight' => 32, }, # Not sure what high.2 is for.. 'high.2' => { 'GDFontSize' => 7, 'GDBaseline' => 7, # 'GDFixedfont' => GD::gdSmallFont, 'GDFixedbase' => 10, 'GDFixedspacing' => 6, 'GDHeight' => 32, }, # v1: Small size - .1 is top line, .2 is bottom. 'small.1' => { 'GDFontSize' => 5, 'GDBaseline' => 7, 'GDFixedfont' => GD::gdTinyFont, # 7 px 'GDFixedbase' => 0, 'GDFixedspacing' => 2, 'GDHeight' => 16, }, 'small.2' => { 'GDFontSize' => 6, 'GDBaseline' => 14, 'GDFixedfont' => GD::gdTinyFont, 'GDFixedbase' => 8, 'GDFixedspacing' => 2, 'GDHeight' => 16, }, # v1: Medium size - .1 is top line, .2 is bottom. 'medium.1' => { 'GDFontSize' => 6, 'GDBaseline' => 6, 'GDFixedfont' => GD::gdTinyFont, 'GDFixedbase' => 0, 'GDFixedspacing' => 3, 'GDHeight' => 16, }, 'medium.2' => { 'GDFontSize' => 7, 'GDBaseline' => 14, 'GDFixedfont' => GD::gdTinyFont, 'GDFixedbase' => 8, 'GDFixedspacing' => 3, 'GDHeight' => 16, }, # v1: Large - only one line. 'large.2' => { 'GDFontSize' => 10, 'GDBaseline' => 13, 'GDFixedfont' => GD::gdSmallFont, # 11 px 'GDFixedbase' => 1, 'GDFixedspacing' => 3, 'GDHeight' => 16, }, # v1: Huge - only one line. 'huge.2' => { 'GDFontSize' => 13, 'GDBaseline' => 13, 'GDFixedfont' => GD::gdLargeFont, # 16 px 'GDFixedbase' => 0, 'GDFixedspacing' => 4, 'GDHeight' => 16, }, ); sub init { loadFonts(); $::d_graphics && msgf("Trying to load GD Library for TTF support: %s\n", $canUseGD ? 'ok' : 'not ok!'); if ($canUseGD) { FONTDIRS: for my $fontFolder (graphicsDirs()) { # Initialize an image for working (1 character=32x32) and some variables... # Try a few different fonts.. for my $fontFile (qw(VeraBd.ttf arialuni.ttf ARIALUNI.TTF CODE2000.TTF Cyberbit.ttf CYBERBIT.TTF)) { $TTFFontFile = catdir($fontFolder, $fontFile); if ($canUseGD && -e $TTFFontFile) { $useTTF = 1; last FONTDIRS; } } } if ($useTTF) { $::d_graphics && msgf("Using TTF for Unicode on Player Display. Font: [$TTFFontFile]\n"); $useTTFCache = 1; %TTFCache = (); # This should be configurable. $gd = GD::Image->new(32, 32); $GDWhite = $gd->colorAllocate(255,255,255); $GDBlack = $gd->colorAllocate(0,0,0); } } else { $::d_graphics && msg("Error while trying to load GD Library: [$gdError]\n"); } } sub gfonthash { return $fonthash; } sub fontnames { my $client = shift; my %fontnames; my $i=0; foreach my $gfont (keys %{$fonthash}) { my $fontname = ${$fonthash->{$gfont}->{line2}}; $fontname =~ s/(\.2)?//g; $fontnames{$fontname} = $fontname; } return \%fontnames; }; sub fontheight { my $fontname = shift; return $fontheight->{$fontname}; } # extent returns the number of rows high a font is rendered (useful for vertical scrolling) # based on char 0x1f, which is a bitmask of the valid rows. # negative values are for top-row fonts sub extent { my $fontname = shift; return $fontextents->{$fontname} || 0; } sub loadExtent { my $fontname = shift; my $extentbytes = string($fontname, chr(0x1f)); # count the number of set bits in the extent bytes (up to 32) my $extent = unpack( '%32b*', $extentbytes ); if ($fontname =~ /\.1/) { $extent = -$extent; } $fonts->{fontextents}->{$fontname} = $extent; $::d_graphics && msg(" extent of: $fontname is $extent\n"); } sub string { my $fontname = shift || return ''; my $string = shift || return ''; my $bits = ''; my $font = $fonts->{$fontname} || do { msg(" Invalid font $fontname\n"); bt(); return ''; }; my $interspace = $font->[0]; my $cursorpos = undef; my $cursorend = 0; my $unpackTemplate = 'C*'; # U - unpacks Unicode chars into ords, much faster than split(//, $string) # C - is needed for older 5.6 perl's if ($] > 5.007) { $unpackTemplate = 'U*'; } # Font size & Offsets -- Optimized for the free Japanese TrueType font # 'sazanami-gothic' from 'waka'. # # They seem to work pretty well for CODE2000 & Cyberbit as well. - dsully my ($GDFontSize, $GDBaseline, $GDFixedfont, $GDFixedbase, $GDWidth, $GDHeight, $GDFixedspacing); my $useTTFNow = 0; my $useBuiltin = 0; if ($useTTF && defined $font2TTF{$fontname}) { $useTTFNow = 1; $GDFontSize = $font2TTF{$fontname}->{'GDFontSize'}; $GDBaseline = $font2TTF{$fontname}->{'GDBaseline'}; $GDFixedfont = $font2TTF{$fontname}->{'GDFixedfont'}; $GDFixedbase = $font2TTF{$fontname}->{'GDFixedbase'}; $GDFixedspacing = $font2TTF{$fontname}->{'GDFixedspacing'}; $GDHeight = $font2TTF{$fontname}->{'GDHeight'}; } for my $ord (unpack($unpackTemplate, $string)) { # 29 == \x1d, 28 == \x1c, 10 == \x0a if ($ord == 29) { $interspace = ''; } elsif ($ord == 28) { $interspace = $font->[0]; } elsif ($ord == 10) { $cursorpos = length($bits); } else { if ($ord > 31 && $useTTFNow) { my $bits_tmp = $useTTFCache ? $TTFCache{$fontname}{$ord} : ''; if ($ord < 256 && defined($GDFixedfont)) { $useBuiltin = 1; } unless ($bits_tmp) { my @GDbounds; $bits_tmp = ''; # Create our canvas. $gd->filledRectangle(0, 0, 31, 31, $GDWhite); # Using a negative color index will # disable anti-aliasing, as described # in the libgd manual page at # http://www.boutell.com/gd/manual2.0.9.html#gdImageStringFT. # if ($useBuiltin) { $gd->string($GDFixedfont, 0, $GDFixedbase, chr($ord), $GDBlack); # Find out how wide the glyph is $GDWidth = 32; WIDTH: for (my $x = $GDWidth; $x >= 0 ; $x--) { for (my $y = 0; $y < $GDHeight; $y++) { last WIDTH if $gd->getPixel($x,$y) == $GDBlack; } $GDWidth--; } # Add spacing $GDWidth += $GDFixedspacing; } else { @GDbounds = $gd->stringFT(-1*$GDBlack, $TTFFontFile, $GDFontSize, 0, 0, $GDBaseline, "&#${ord};"); $GDWidth = $GDbounds[2]; } # Construct the bitmap for (my $x = 0; $x <= $GDWidth; $x++) { for (my $y = 0; $y < $GDHeight; $y++) { $bits_tmp .= $gd->getPixel($x,$y) == $GDBlack ? 1 : 0 } } $bits_tmp = pack("B*", $bits_tmp); $TTFCache{$fontname}{$ord} = $bits_tmp if $useTTFCache; } if (defined($cursorpos) && !$cursorend) { $cursorend = length($bits_tmp) / length($font->[$ord0a]); } $bits .= $bits_tmp; } else { # We don't handle anything outside ISO-8859-1 # right now in our non-TTF bitmaps. if ($ord > 31) { $ord = 63; # 63 == '?' } if (defined($cursorpos) && !$cursorend) { $cursorend = length($font->[$ord])/ length($font->[$ord0a]); } $bits .= $font->[$ord] . $interspace; } } } if (defined($cursorpos)) { $bits |= ($char0 x $cursorpos) . ($font->[$ord0a] x $cursorend); } return $bits; } sub measureText { my $fontname = shift; my $string = shift; my $bits = string($fontname, $string); return 0 if (!$fontname || !$fontheight->{$fontname}); my $len = length($bits)/($fontheight->{$fontname}/8); return $len; } sub graphicsDirs { my @dirs = catdir('/usr/share/slimserver',"Graphics"); if (Slim::Utils::OSDetect::OS() eq 'mac') { push @dirs, $ENV{'HOME'} . "/Library/SlimDevices/Graphics/"; push @dirs, "/Library/SlimDevices/Graphics/"; } # For the ttf font.... push @dirs, "/usr/share/fonts/truetype/ttf-bitstream-vera/"; return @dirs; } sub fontCacheFile { return catdir( Slim::Utils::Prefs::get('cachedir'), Slim::Utils::OSDetect::OS() eq 'unix' ? 'fontcache' : 'fonts.bin'); } #returns a reference to a hash of filenames/external names and newest modification time sub fontfiles { my %fontfilelist = (); my $newest = 0; foreach my $fontfiledir (graphicsDirs()) { if (opendir(DIR, $fontfiledir)) { foreach my $fontfile ( sort(readdir(DIR)) ) { if ($fontfile =~ /(.+)\.font.bmp$/) { $::d_graphics && msg(" fontfile entry: $fontfile\n"); $fontfilelist{$1} = catdir($fontfiledir, $fontfile); my $moddate = (stat($fontfilelist{$1}))[9]; $newest = $moddate if $moddate > $newest; } } closedir(DIR); } } return ($newest, %fontfilelist); } sub loadFonts { my $forceParse = shift; my ($newest, %fontfiles) = fontfiles(); my $fontCache = fontCacheFile(); # use stored fontCache if newer than all font files if (!$forceParse && -r $fontCache && ($newest < (stat($fontCache))[9])) { # check cache for consitency my $cacheOK = 1; $::d_graphics && msg( "Retrieving font data from font cache: $fontCache\n"); eval { $fonts = retrieve($fontCache); }; $::d_graphics && $@ && msg(" Tried loading fonts: [EMAIL PROTECTED]"); if (!$@ && defined $fonts && defined($fonts->{fonthash}) && defined($fonts->{fontheight}) && defined($fonts->{fontextents})) { $fonthash = $fonts->{fonthash}; $fontheight = $fonts->{fontheight}; $fontextents = $fonts->{fontextents}; } else { $cacheOK = 0; } # check for font files being removed foreach my $font (keys %{$fontheight}) { $cacheOK = 0 if !exists($fontfiles{$font}); } # check for new fonts being added (with old modification date) foreach my $font (keys %fontfiles) { $cacheOK = 0 if !exists($fontheight->{$font}); } return if $cacheOK; $::d_graphics && msg( " font cache contains old data - reparsing fonts\n"); } # otherwise clear data and parse all font files $fonts = {}; foreach my $font (keys %fontfiles) { $::d_graphics && msg( "Now parsing: $font\n"); if ($font =~ m/(.*?).(\d)/i) { $fonts->{fonthash}->{$1}->{"line$2"} = \$font; $fonts->{fonthash}->{$1}->{"overlay$2"} = \$font; $fonts->{fonthash}->{$1}->{"center$2"} = \$font; } my ($fontgrid, $height) = parseBMP($fontfiles{$font}); $::d_graphics && msg( " height of: $font is ".($height - 1)."\n"); $fonts->{fontheight}->{$font} = $height - 1; $fonts->{$font} = parseFont($fontgrid); $fonts->{fontextent}->{$font} = loadExtent($font); } # set to \undef if undefined (e.g. font is double height and only one line defined) foreach my $name (keys %{$fonts->{fonthash}}) { $fonts->{fonthash}->{$name}->{line1} = \undef if !exists($fonts->{fonthash}->{$name}->{line1}); $fonts->{fonthash}->{$name}->{line2} = \undef if !exists($fonts->{fonthash}->{$name}->{line2}); $fonts->{fonthash}->{$name}->{overlay1} = \undef if !exists($fonts->{fonthash}->{$name}->{overlay1}); $fonts->{fonthash}->{$name}->{overlay2} = \undef if !exists($fonts->{fonthash}->{$name}->{overlay2}); $fonts->{fonthash}->{$name}->{center1} = \undef if !exists($fonts->{fonthash}->{$name}->{center1}); $fonts->{fonthash}->{$name}->{center2} = \undef if !exists($fonts->{fonthash}->{$name}->{center2}); } $fonthash = $fonts->{fonthash}; $fontheight = $fonts->{fontheight}; $fontextents = $fonts->{fontextents}; $::d_graphics && msg( "Writing font cache: $fontCache\n"); store($fonts, $fontCache); } # parse the array of pixels ino a font table sub parseFont { use bytes; my $g = shift; my $fonttable; my $bottomIndex = scalar(@{$g}) - 1; my $bottomRow = $g->[$bottomIndex]; my $width = @{scalar($bottomRow)}; my $charIndex = -1; for (my $i = 0; $i < $width; $i++) { #print "\n"; next if ($bottomRow->[$i]); last if (!defined($bottomRow->[$i])); $charIndex++; my @column = (); while (!$bottomRow->[$i]) { for (my $j = 0; $j < $bottomIndex; $j++) { push @column, $g->[$j][$i]; #print $g->[$j][$i] ? '*' : ' '; } #print "\n"; $i++; } $fonttable->[$charIndex] = pack("B*", join('',@column)); #print $charIndex . " " . chr($charIndex) . " " . scalar(@column) . " ". length($fonttable->[$charIndex]) . " i: $i width: $width\n"; if ($charIndex == 0 && unpack( '%32b*', $fonttable->[$charIndex]) > 0) { # pixels in interspace character indicate no interspace $fonttable->[$charIndex] = ''; } } #print "done fonttable\n"; return $fonttable; } # parses a monochrome, uncompressed BMP file into an array for font data sub parseBMP { use bytes; my $fontfile = shift; my $fontstring; my @font; # slurp in bitmap file { local $/; open(my $fh, $fontfile) or do { errorMsg(" couldn't open fontfile $fontfile\n"); return undef; }; binmode $fh; $fontstring = <$fh>; } my ($type, $fsize, $offset, $biSize, $biWidth, $biHeight, $biPlanes, $biBitCount, $biCompression, $biSizeImage ) = unpack("a2 V xx xx V V V V v v V V xxxx xxxx xxxx xxxx", $fontstring); if ($type ne "BM") { msg(" No BM header on $fontfile\n"); return undef; } if ($fsize ne -s $fontfile) { msg(" Bad size in $fontfile\n"); return undef; } if ($biPlanes ne 1) { msg(" Planes must be one, not $biPlanes in $fontfile\n"); return undef; } if ($biBitCount ne 1) { msg(" Font files must be one bpp, not $biBitCount in $fontfile\n"); return undef; } if ($biCompression ne 0) { msg(" Font files must be uncompressed in $fontfile\n"); return undef; } # skip over the BMP header and the color table $fontstring = substr($fontstring, $offset); my $bitsPerLine = $biWidth - ($biWidth % 32); $bitsPerLine += 32 if ($biWidth % 32); # round up to 32 pixels wide for (my $i = 0 ; $i < $biHeight; $i++) { my @line = (); my $bitstring = substr(unpack( "B$bitsPerLine", substr($fontstring, $i * $bitsPerLine / 8) ), 0,$biWidth); my $bsLength = length($bitstring); # Surprisingly, this loop is about 20% faster than doing split(//, $bitString) for (my $j = 0; $j < $bsLength; $j++) { $line[$j] = substr($bitstring, $j, 1); } $font[$biHeight-$i-1] = [EMAIL PROTECTED]; } return ([EMAIL PROTECTED], $biHeight); } 1; __END__ # Local Variables: # tab-width:4 # indent-tabs-mode:t # End: