<?php
/**
 * class.p2f.php
 *
 * Provides read/write capability for the Plasma P2F font file format
 *
 * PHP version 5
 *
 * @author     Lewis Johnston (OHB) <me@orangehairedboy.com>
 * @copyright  2010 Lewis Johnston
 * @version    0.71
 * @link       http://uru.orangehairedboy.com/x/class.p2f.phps
 * @since      File available since Release 0.5
 * @license    http://sam.zoy.org/wtfpl/COPYING
 */

/*
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

/*
Note: I wrote over the course of like 3 days. I know it needs a lot of work.

Don't: Complain
Do   : Fix
*/

class P2F {
    
// Font definition is stored publically in the following variables
    
public $name;
    public 
$size 0;
    public 
$flags 0;
    public 
$width 0;
    public 
$height 0;
    public 
$maxHeight 0;
    public 
$BPP 0;
    public 
$firstChar 0;
    public 
$characters = array();
    
    
// read a P2F file
    
public function readP2F$file ) {
        
$file file_get_contents$file );
        
        
// make sure there's a header
        
if ( strlen$file ) < 274 ) return FALSE;
        
        
$this->name trim$this->readBytes$file 256 ) );
        
$this->size ord$this->readBytes$file ) );
        
$this->flags $this->readInt$file );
        
$this->width $this->readInt$file );
        
$this->height $this->readInt$file );
        
$this->maxHeight $this->readInt$file );
        
$this->BPP ord$this->readBytes$file ) );
        
        
$imageSize = ( $this->BPP $this->width $this->height ) / 8;
        
        
// make sure there's an image
        
if ( strlen$file ) < $imageSize ) return FALSE;
        
        
$imageData $this->readBytes$file $imageSize );
        
        
// make sure there's a footer
        
if ( strlen$file ) < ) return FALSE;
        
        
$this->firstChar $this->readShort$file );
        
$characterCount $this->readInt$file );
        
        
// make sure there's enough character data
        
if ( strlen$file ) < ( $characterCount 20 ) ) return FALSE;
        
        
$this->characters = array();
        for( 
$x $this->firstChar $x $this->firstChar $characterCount $x ++ ) {
            
$this->characters[$x] = array(
                
'bitmapOffset' => $this->readInt$file ) ,
                
'height' => $this->readInt$file ) ,
                
'baseline' => $this->readInt$file ) ,
                
'leftkern' => $this->readFloat$file ) ,
                
'rightkern' => $this->readFloat$file )
            );
            
$this->characters[$x]['image'] = $this->readBytes$imageData $this->characters[$x]['height'] * $this->width );
        }
        return 
TRUE;
    }
    
    
// build a P2F file from the stored font data
    
public function getP2F() {
        
$this->BPP 8// patch
        
$imageData "";
        
$P2F "";
        for( 
$x $x <= 255 $x ++ ) $imageData .= $this->characters[$x]['image'];
        
$this->writeBytes$P2F $this->name str_repeatchr) , 256 strlen$this->name ) ) );
        
$this->writeBytes$P2F chr$this->size ) );
        
$this->writeInt$P2F $this->flags );
        
$this->writeInt$P2F $this->width );
        
$this->writeInt$P2F strlen$imageData ) / $this->width );
        
$this->writeInt$P2F $this->maxHeight );
        
$this->writeBytes$P2F chr$this->BPP ) );
        
$this->writeBytes$P2F $imageData );
        
$this->writeShort$P2F $this->firstChar );
        
$this->writeInt$P2F count$this->characters ) );
        for( 
$x $x 256 $x ++ ) {
            if ( ! 
$this->characters[$x]['baseline'] ) $this->characters[$x]['baseline'] = 0// patch
            
$this->writeInt$P2F $this->characters[$x]['bitmapOffset'] );
            
$this->writeInt$P2F $this->characters[$x]['height'] );
            
$this->writeInt$P2F , (int) $this->characters[$x]['baseline'] );
            
$this->writeFloat$P2F $this->characters[$x]['leftkern'] );
            
$this->writeFloat$P2F $this->characters[$x]['rightkern'] );
        }
        return 
$P2F;
    }
    
    
// read a P2FC file (P2F Compressed)
    
public function readP2FC$file ) {
        
$file file_get_contents$file );
        
$this->readP2FCString$file );
    }
    
    
// read a P2FC from a string
    
public function readP2FCString$string ) {
        
$file unserializegzinflate$string ) );
        
$this->name $file['name'];
        
$this->size $file['size'];
        
$this->width $file['width'];
        
$this->height $file['height'];
        
$this->maxHeight $file['maxHeight'];
        
$this->BPP $file['bpp'];
        
$this->firstChar $file['firstChar'];
        
$this->characters $file['chars'];
    }
    
    
// build a P2FC file (P2F Compressed) from the stored font data
    
public function getP2FC() {
        
$this->BPP 8;
        
$font = array(
            
'name' => $this->name ,
            
'size' => $this->size ,
            
'flags' => $this->flags ,
            
'width' => $this->width ,
            
'height' => $this->height ,
            
'maxHeight' => $this->maxHeight ,
            
'bpp' => $this->BPP ,
            
'firstChar' => $this->firstChar ,
            
'chars' => $this->characters
        
);
        return 
gzdeflateserialize$font ) , );
    }
    
    
// write text to the provided image using the P2F font
    
function imageP2Ftext( &$image $padLeft $padTop $color $text ) {
        if ( ! 
$this->name ) return FALSE;
        
imagealphablending$image true );
        
$colors = array();
        
$lineLeft 0;
        
$color imagecolorsforindex$image $color );
        for( 
$x $x strlen$text ) ; $x ++ ) {
            
$char ordsubstr$text $x ) );
            
            for( 
$y $y strlen$this->characters[$char]['image'] ) ; $y ++ ) {
                
$cOrd 255 ordsubstr$this->characters[$char]['image'] , $y ) );
                if ( 
$cOrd == 255 ) continue;
                
$cOrd ceil$cOrd );
                
$cY floor$y $this->width );
                
$cX $y - ( $cY $this->width );
                if ( ! isset( 
$colors[$cOrd] ) ) {
                    
$check imagecolorclosestalpha$image $color['red'] , $color['green'] , $color['blue'] , $cOrd );
                    
$check2 imagecolorsforindex$image $check );
                    if ( 
$check2['alpha'] == $cOrd ) {
                        
$colors[$cOrd] = $check;
                    } else {
                        
$colors[$cOrd] = imagecolorallocatealpha$image $color['red'] , $color['green'] , $color['blue'] , $cOrd );
                    }
                }
                
$charTop = ( $this->characters[$char]['baseline'] * -);
                
imagesetpixel$image $padLeft $lineLeft $this->characters[$char]['leftkern'] + $cX $padTop $charTop $cY $colors[$cOrd] );
            }
            
$lineLeft += $this->width $this->characters[$char]['leftkern'] + $this->characters[$char]['rightkern'];
        }
        
imagealphablending$image FALSE );
    }
    
    
// calculate the height, width, and top/left starting point of a line of text in the P2F font
    
function calculateP2FTextBox$text ) {
        if ( ! 
$this->name ) return FALSE;
        
$width $this->width * ( strlen$text ) - );
        
$heightAbove 0;
        
$heightBelow 0;
        
$left 0;
        for( 
$x $x strlen$text ) ; $x ++ ) {
            
$char ordsubstr$text $x ) );
            
$BL $this->characters[$char]['baseline'];
            if ( 
$BL $this->maxHeight $BL 0;
            
$heightAbove max$heightAbove $BL );
            if ( 
$this->characters[$char]['baseline'] - $this->characters[$char]['height'] < ) {
                
$heightBelow max$heightBelow abs$this->characters[$char]['baseline'] - $this->characters[$char]['height'] ) );
            }
            if ( 
$x ) {
                
$width += $this->characters[$char]['leftkern'];
            } else {
                
$left $this->characters[$char]['leftkern'] * -1;
            }
            if ( 
$x < ( strlen$text ) - ) ) {
                
$width += $this->characters[$char]['rightkern'];
            } else {
                
$right 0;
                for( 
$y strlen$this->characters[$char]['image'] ) - $y >= $y -- ) {
                    
$cX $y - ( floor$y $this->width ) * $this->width );
                    if ( 
ordsubstr$this->characters[$char]['image'] , $y ) ) > ) {
                        
$right max$right , ( $cX ) );
                    }
                }
                
$width += $right;
            }
        }
        return array(
            
'width' => $width ,
            
'height' => ( $heightAbove $heightBelow ) ,
            
'left' => $left ,
            
'top' => $heightAbove
        
);
    }
    
    
// convert a TTF to P2F
    
public function createFromTTF$font $size $charset "ISO-8859-1" $bold FALSE $italics FALSE ) {
        
// this is probably going to take a while, but let's limit it at 5 minutes just in case!
        
set_time_limit600 );

        
$name $this->getTTFName$font );
        if ( ! 
$name ) return FALSE;
        
        
$size round$size );
        
$flags 0;
        if ( 
$bold $flags += 1;
        if ( 
$italics $flags += 2;
        
$maxWidth 0;
        
$maxHeight 0;
        
$boxes = array();
        
$chars = array();
        
        for( 
$x $x 256 $x ++ ) {
            
$chars[$x] = array( 'symbol' => iconv$charset "UTF-8" chr$x ) ) );
        }

        for( 
$x $x 256 $x ++ ) {
            
$boxes[$x] = $this->calculateTTFTextBox$size $font $chars[$x]['symbol'] );
            if ( 
$x == 32 $boxes[$x]['height'] = 1;
            if ( 
$boxes[$x]['height'] < $boxes[$x]['height'] = 1;
            
$chars[$x]["height"] = $boxes[$x]['height'] + 1;
            
$chars[$x]["baseline"] = (int) $boxes[$x]['top'];
            
$chars[$x]["leftkern"] = 0;
            
$chars[$x]["rightkern"] = 0;
            
$maxWidth max$maxWidth $boxes[$x]['width'] );
            
$maxHeight max$maxHeight $boxes[$x]['height'] );
        }

        
// find spacing for TAB and SPACE
        
foreach( array( 32 ) as $x ) {
            
$box imagettfbbox$size $font $chars[$x]['symbol'] );
            
$max_x max( array($box[0], $box[2], $box[4], $box[6]) );
            
$min_x min( array($box[0], $box[2], $box[4], $box[6]) );
            
$charWidth $max_x $min_x;
            
$chars[$x]['rightkern'] = $charWidth $maxWidth;
        }

        
// autokern!
        
$autoKern = array();
        
$imgA imagecreatetruecolor$maxWidth $maxHeight );
        
$blackA imagecolorallocate$imgA );
        
$whiteA imagecolorallocate$imgA 255 255 255 );
        
$imgB imagecreatetruecolor$maxWidth $maxHeight );
        
$blackB imagecolorallocate$imgB );
        
$whiteB imagecolorallocate$imgB 255 255 255 );
        for( 
$x 33 $x 256 $x ++ ) {
            if ( 
$chars[$x]['symbol'] == '' ) continue;
            if ( 
$x >= 127 && $x <= 160 ) continue;
            
imagefilledrectangle$imgA $maxWidth $maxHeight $whiteA );
            
imagettftext$imgA $size $boxes[$x]['left'] , $boxes[$x]['top'] + ( $maxHeight $boxes[$x]['height'] ) , $blackA $font $chars[$x]['symbol'] );

            for( 
$y 33 $y 256 $y ++ ) {
                if ( 
$chars[$y]['symbol'] == '' ) continue;
                if ( 
$y >= 127 && $y <= 160 ) continue;
                
imagefilledrectangle$imgB $maxWidth $maxHeight $whiteB );
                
imagettftext$imgB $size $boxes[$x]['left'] , $boxes[$x]['top'] + ( $maxHeight $boxes[$x]['height'] ) , $blackB $font $chars[$x]['symbol'] . $chars[$y]['symbol'] );

                
$found FALSE;
                for( 
$ix $ix max$boxes[$x]['width'] , $boxes[$y]['width'] ) ; $ix ++ ) {
                    for( 
$iy $maxHeight $iy >= $iy -- ) {
                        
$cA imagecolorsforindex$imgA imagecolorat$imgA $ix $iy ) );
                        
$cB imagecolorsforindex$imgB imagecolorat$imgB $ix $iy ) );
                        if ( 
$cA['red'] != $cB['red'] || $cA['green'] != $cB['green'] || $cA['blue'] != $cB['blue'] ) $found TRUE;
                        if ( 
$found ) break;
                    }
                    if ( 
$found ) break;
                }
                
$autoKern[$x][$y] = $ix $boxes[$x]['width'];
            }
        }
        
imagedestroy$imgB );
        
imagedestroy$imgA );
        
        
// Parse autokern data
        
for( $x 33 $x 256 $x ++ ) {
            if ( 
$chars[$x]['symbol'] == '' ) continue;
            
$itemsL = array();
            
$itemsR = array();
            for( 
$y 33 $y 256 $y ++ ) {
                if ( 
$chars[$y]['symbol'] == '' ) continue;
                if ( isset( 
$autoKern[$y][$x] ) ) $itemsL[] = $autoKern[$y][$x];
                if ( isset( 
$autoKern[$x][$y] ) ) $itemsR[] = $autoKern[$x][$y];
            }
            
$chars[$x]['leftkern'] = $this->average$itemsL );
            
$chars[$x]['rightkern'] = $this->average$itemsR ) - ( $maxWidth $boxes[$x]['width'] );
        }
        
        
// build the P2F character images
        
$offset 0;
        for( 
$x $x 256 $x ++ ) {
            
$chars[$x]['image'] = "";
            
$chars[$x]['bitmapOffset'] = $offset;

            if ( 
$boxes[$x]['height'] > ) {
                
$img imagecreatetruecolor$maxWidth $boxes[$x]['height'] + );
                
$black imagecolorallocate$img );
                
$white imagecolorallocate$img 255 255 255 );
                
imagefilledrectangle$img $maxWidth $boxes[$x]['height'] , $black );
                
imagettftext$img $size $boxes[$x]['left'] , $boxes[$x]['top'] , $white $font $chars[$x]['symbol'] );
                
$found FALSE;
                for( 
$ix $maxWidth $ix >= $ix -- ) {
                    for( 
$iy $iy $boxes[$x]['height'] ; $iy ++ ) {
                        if ( 
imagecolorat$img $ix $iy ) > $found TRUE;
                        if ( 
$found ) break;
                    }
                    if ( 
$found ) break;
                }
                for( 
$iy $iy <= $boxes[$x]['height'] ; $iy ++ ) {
                    for( 
$ix $ix $maxWidth $ix ++ ) {
                        
$rgb imagecolorat$img $ix $iy );
                        
$color floor( ( ( ( $rgb >> 16 ) & 0xFF ) + ( ( $rgb >> ) & 0xFF ) + ( $rgb 0xFF ) ) / );
                        
$chars[$x]['image'] .= chr$color );
                    }
                }
                
imagedestroy$img );
            } else {
                die( 
"Height 0 for char {$x});
            }
            
$offset += strlen$chars[$x]['image'] );
            unset( 
$chars[$x]['symbol'] );
        }

        
// should always be zero for a TTF right???
        
$firstCharacter 0;
        
        
$this->name $name;
        
$this->size $size;
        
$this->flags $flags;
        
$this->width $maxWidth;
        
$this->maxHeight $maxHeight;
        
$this->BPP 8;
        
$this->firstChar $firstCharacter;
        
$this->characters $chars;
        
        return 
$this;
    }
    
    
// extracts the name of a font from the TTF's 'name' table
    
public function getTTFName$font ) {
        
$stream file_get_contents$font );
        
$number_of_tables hexdec$this->dec2ord$stream[4] ) . $this->dec2ord$stream[5] ) );
        for ( 
$i $i $number_of_tables $i ++ ) {
            
$tag $stream[12+$i*16].$stream[12+$i*16+1].$stream[12+$i*16+2].$stream[12+$i*16+3];
            if ( 
$tag == 'name' ) {
                
$ntOffset hexdec(
                    
$this->dec2ord$stream[12+$i*16+8] ) . $this->dec2ord$stream[12+$i*16+8+1] ) .
                    
$this->dec2ord$stream[12+$i*16+8+2] ) . $this->dec2ord$stream[12+$i*16+8+3] )
                );
                
$offset_storage_dec hexdec$this->dec2ord$stream[$ntOffset+4] ) . $this->dec2ord$stream[$ntOffset+5] ) );
                
$number_name_records_dec hexdec$this->dec2ord$stream[$ntOffset+2] ) . $this->dec2ord$stream[$ntOffset+3] ) );
            }
        }
        if ( ! 
$ntOffset ) return FALSE;
        
$font_tags = array();
        
$storage_dec $offset_storage_dec $ntOffset;
        
$storage_hex strtoupperdechex$storage_dec ) );
        for ( 
$j $j $number_name_records_dec $j ++ ) {
            
$recOffset $ntOffset $j 12;
            
$platform_id_dec hexdec$this->dec2ord$stream[$recOffset] ) . $this->dec2ord$stream[$recOffset+1] ) );
            
$name_id_dec hexdec$this->dec2ord$stream[$recOffset+6] ) . $this->dec2ord$stream[$recOffset+7] ) );
            
$string_length_dec hexdec$this->dec2ord$stream[$recOffset+8] ) . $this->dec2ord$stream[$recOffset+9] ) );
            
$string_offset_dec hexdec$this->dec2ord$stream[$recOffset+10] ) . $this->dec2ord$stream[$recOffset+11] ) );
            if ( !empty( 
$name_id_dec ) and empty( $font_tags[$name_id_dec] ) ) {
                for( 
$l $l $string_length_dec $l ++ ) {
                    if ( 
ord$stream[$storage_dec+$string_offset_dec+$l] ) == '0' ) continue;
                    
$font_tags[$name_id_dec] .= ( $stream[$storage_dec+$string_offset_dec+$l] );
                }
            }
        }
        if ( ! isset( 
$font_tags[4] ) || ! trim$font_tags[4] ) ) return FALSE;
        return 
trim$font_tags[4] );
    }
    
    
// calculate the height, width, and top/left starting point of a line of text in the TTF font
    // Note, while this uses the imagettfbbox function to get the bounding box, this function returns the /actual/ size
    
function calculateTTFTextBox$font_size $font_angle $font_file $text ) {
        
$box   imagettfbbox$font_size $font_angle $font_file $text );
        if( ! 
$box ) return FALSE;
        
$min_x min( array( $box[0] , $box[2] , $box[4] , $box[6] ) );
        
$max_x max( array( $box[0] , $box[2] , $box[4] , $box[6] ) );
        
$min_y min( array( $box[1] , $box[3] , $box[5] , $box[7] ) );
        
$max_y max( array( $box[1] , $box[3] , $box[5] , $box[7] ) );
        
$width  = ( $max_x $min_x );
        
$height = ( $max_y $min_y );
        
$left   abs$min_x ) + $width;
        
$top    abs$min_y ) + $height;
        if ( 
$width ) return FALSE;
        if ( 
$height ) return FALSE;
        
$img imagecreatetruecolor$width $height );
        
$white imagecolorallocate$img 255 255 255 );
        
$black imagecolorallocate$img );
        
imagefilledrectangle$img imagesx$img ) , imagesy$img ) , $black );
        
imagettftext$img $font_size $font_angle $left $top $white $font_file $text );
        
$rleft  $w4 $width 5;
        
$rright 0;
        
$rbottom   0;
        
$rtop $h4 $height 5;
        for( 
$x $x $w4 $x++ ) {
            for( 
$y 0$y $h4$y++ ) {
                if( 
imagecolorat$img $x $y ) ) {
                    
$rleft min$rleft $x );
                    
$rright max$rright $x );
                    
$rtop min$rtop $y );
                    
$rbottom max$rbottom $y );
                }
            }
        }
        
imagedestroy$img );
        return array(
            
"left" => $left $rleft ,
            
"top" => $top  $rtop ,
            
"width" => $rright $rleft ,
            
"height" => $rbottom $rtop 1
        
);
    }
    
    
// calculate a mathematical median
    
private function median$items ) {
        
sort$items );
        
$n count$items );
        
$h intval$n );
        if ( 
$n == ) return round( ( $items[$h] + $items[$h-1] ) / );
        return 
$items[$h];
    }

    private function 
average$items ) {
        if ( 
count$items ) == ) return 0;
        foreach( 
$items as $item $total += $item;
        return 
round$total count$items ) , );
    }
    
    
// remove a number of bytes from a string and return the removed portion
    
private function readBytes( &$string $bytes ) {
        
$buffer substr$string $bytes );
        
$string substr$string $bytes );
        return 
$buffer;
    }
    
    
// read an INT from a string
    
private function readInt( &$string ) {
        
$bytes $this->readBytes$string );
        
$int = ( 16777216 ordsubstr$bytes ) ) ) +
               ( 
65536 ordsubstr$bytes ) ) ) +
               ( 
256 ordsubstr$bytes ) ) ) +
               
ordsubstr$bytes ) );
        return 
$int;
    }
    
    
// read a SHORT from a string
    
private function readShort( &$string ) {
        
$bytes $this->readBytes$string );
        
$int = ( 256 ordsubstr$bytes ) ) ) +
               
ordsubstr$bytes ) );
        return 
$int;
    }
    
    
// read a FLOAT from a string
    
private function readFloat( &$string ) {
        
$bytes $this->readBytes$string );
        
$float unpack('f'$bytes );
        return 
$float[1];
    }
    
    
// write bytes to a string
    
private function writeBytes( &$string $bytes ) {
        
$string .= $bytes;
    }
    
    
// write an INT to a string
    
private function writeInt( &$string $int ) {
        
$iString "";
        
$x floor$int 16777216 );
        
$iString chr$x );
        
$int -= ( 16777216 $x );
        
$x floor$int 65536 );
        
$iString chr$x ) . $iString;
        
$int -= ( 65536 $x );
        
$x floor$int 256 );
        
$iString chr$x ) . $iString;
        
$int -= ( 256 $x );
        
$iString chr$int ) . $iString;
        
$string .= $iString;
    }
    
    
// write a SHORT to a string
    
private function writeShort( &$string $short ) {
        
$iString "";
        
$x floor$short 256 );
        
$iString chr$x ) . $iString;
        
$short = ( 256 $x );
        
$iString chr$short ) . $iString;
        
$string .= $iString;
    }
    
    
// write a FLOAT to a string
    
private function writeFloat( &$string $float ) {
        
$float pack'f' $float );
        
$string .= $float;
    }
    
    
// I stole this function. It's very badly named.
    // It takes 1 byte and returns the HEX code for it.
    // Should probably be called char2hex( $char )
    
private function dec2ord$dec ) {
        return 
str_repeat'0' strlen( ( $hex strtoupperdechexord$dec ) ) ) ) ) ) . $hex;
    }
}
?>