Flowing block

Informations

Author: Damon Kohler
License: GPL

Description

This script allows writing text in a block in much the same way the Write function works. The block is defined in a manner similar to MultiCell (width, line height, border, alignment and filling color). It is possible to add content with WriteFlowingBlock and change font attributes (SetFont and SetFontSize) between two calls.
Changes in text color are not supported; however, such a change could be easily added.

This was written as part of the phpFOP project.

Source

<?php
require('fpdf.php');

class PDF_FlowingBlock extends FPDF
{

    protected $flowingBlockAttr;

    function saveFont()
    {

        $saved = array();

        $saved[ 'family' ] = $this->FontFamily;
        $saved[ 'style' ] = $this->FontStyle;
        $saved[ 'sizePt' ] = $this->FontSizePt;
        $saved[ 'size' ] = $this->FontSize;
        $saved[ 'curr' ] = $this->CurrentFont;

        return $saved;

    }

    function restoreFont( $saved )
    {

        $this->FontFamily = $saved[ 'family' ];
        $this->FontStyle = $saved[ 'style' ];
        $this->FontSizePt = $saved[ 'sizePt' ];
        $this->FontSize = $saved[ 'size' ];
        $this->CurrentFont = $saved[ 'curr' ];

        if( $this->page > 0)
            $this->_out( sprintf( 'BT /F%d %.2F Tf ET', $this->CurrentFont[ 'i' ], $this->FontSizePt ) );

    }

    function newFlowingBlock( $w, $h, $b = 0, $a = 'J', $f = 0 )
    {

        // cell width in points
        $this->flowingBlockAttr[ 'width' ] = $w * $this->k;

        // line height in user units
        $this->flowingBlockAttr[ 'height' ] = $h;

        $this->flowingBlockAttr[ 'lineCount' ] = 0;

        $this->flowingBlockAttr[ 'border' ] = $b;
        $this->flowingBlockAttr[ 'align' ] = $a;
        $this->flowingBlockAttr[ 'fill' ] = $f;

        $this->flowingBlockAttr[ 'font' ] = array();
        $this->flowingBlockAttr[ 'content' ] = array();
        $this->flowingBlockAttr[ 'contentWidth' ] = 0;

    }

    function finishFlowingBlock()
    {

        $maxWidth = $this->flowingBlockAttr[ 'width' ];

        $lineHeight = $this->flowingBlockAttr[ 'height' ];

        $border = $this->flowingBlockAttr[ 'border' ];
        $align = $this->flowingBlockAttr[ 'align' ];
        $fill = $this->flowingBlockAttr[ 'fill' ];

        $content = $this->flowingBlockAttr[ 'content' ];
        $font = $this->flowingBlockAttr[ 'font' ];

        // set normal spacing
        $this->_out( sprintf( '%.3F Tw', 0 ) );

        // print out each chunk

        // the amount of space taken up so far in user units
        $usedWidth = 0;

        foreach ( $content as $k => $chunk )
        {

            $b = '';

            if ( is_int( strpos( $border, 'B' ) ) )
                $b .= 'B';

            if ( $k == 0 && is_int( strpos( $border, 'L' ) ) )
                $b .= 'L';

            if ( $k == count( $content ) - 1 && is_int( strpos( $border, 'R' ) ) )
                $b .= 'R';

            $this->restoreFont( $font[ $k ] );

            // if it's the last chunk of this line, move to the next line after
            if ( $k == count( $content ) - 1 )
                $this->Cell( ( $maxWidth / $this->k ) - $usedWidth + 2 * $this->cMargin, $lineHeight, $chunk, $b, 1, $align, $fill );
            else
                $this->Cell( $this->GetStringWidth( $chunk ), $lineHeight, $chunk, $b, 0, $align, $fill );

            $usedWidth += $this->GetStringWidth( $chunk );

        }

    }

    function WriteFlowingBlock( $s )
    {

        // width of all the content so far in points
        $contentWidth =& $this->flowingBlockAttr[ 'contentWidth' ];

        // cell width in points
        $maxWidth = $this->flowingBlockAttr[ 'width' ];

        $lineCount =& $this->flowingBlockAttr[ 'lineCount' ];

        // line height in user units
        $lineHeight = $this->flowingBlockAttr[ 'height' ];

        $border = $this->flowingBlockAttr[ 'border' ];
        $align = $this->flowingBlockAttr[ 'align' ];
        $fill = $this->flowingBlockAttr[ 'fill' ];

        $content =& $this->flowingBlockAttr[ 'content' ];
        $font =& $this->flowingBlockAttr[ 'font' ];

        $font[] = $this->saveFont();
        $content[] = '';

        $currContent =& $content[ count( $content ) - 1 ];

        // where the line should be cutoff if it is to be justified
        $cutoffWidth = $contentWidth;

        // for every character in the string
        for ( $i = 0; $i < strlen( $s ); $i++ )
        {

            // extract the current character
            $c = $s[ $i ];

            // get the width of the character in points
            $cw = $this->CurrentFont[ 'cw' ][ $c ] * ( $this->FontSizePt / 1000 );

            if ( $c == ' ' )
            {

                $currContent .= ' ';
                $cutoffWidth = $contentWidth;

                $contentWidth += $cw;

                continue;

            }

            // try adding another char
            if ( $contentWidth + $cw > $maxWidth )
            {

                // won't fit, output what we have
                $lineCount++;

                // contains any content that didn't make it into this print
                $savedContent = '';
                $savedFont = array();

                // first, cut off and save any partial words at the end of the string
                $words = explode( ' ', $currContent );

                // if it looks like we didn't finish any words for this chunk
                if ( count( $words ) == 1 )
                {

                    // save and crop off the content currently on the stack
                    $savedContent = array_pop( $content );
                    $savedFont = array_pop( $font );

                    // trim any trailing spaces off the last bit of content
                    $currContent =& $content[ count( $content ) - 1 ];

                    $currContent = rtrim( $currContent );

                }

                // otherwise, we need to find which bit to cut off
                else
                {

                    $lastContent = '';

                    for ( $w = 0; $w < count( $words ) - 1; $w++)
                        $lastContent .= "{$words[ $w ]} ";

                    $savedContent = $words[ count( $words ) - 1 ];
                    $savedFont = $this->saveFont();

                    // replace the current content with the cropped version
                    $currContent = rtrim( $lastContent );

                }

                // update $contentWidth and $cutoffWidth since they changed with cropping
                $contentWidth = 0;

                foreach ( $content as $k => $chunk )
                {

                    $this->restoreFont( $font[ $k ] );

                    $contentWidth += $this->GetStringWidth( $chunk ) * $this->k;

                }

                $cutoffWidth = $contentWidth;

                // if it's justified, we need to find the char spacing
                if( $align == 'J' )
                {

                    // count how many spaces there are in the entire content string
                    $numSpaces = 0;

                    foreach ( $content as $chunk )
                        $numSpaces += substr_count( $chunk, ' ' );

                    // if there's more than one space, find word spacing in points
                    if ( $numSpaces > 0 )
                        $this->ws = ( $maxWidth - $cutoffWidth ) / $numSpaces;
                    else
                        $this->ws = 0;

                    $this->_out( sprintf( '%.3F Tw', $this->ws ) );

                }

                // otherwise, we want normal spacing
                else
                    $this->_out( sprintf( '%.3F Tw', 0 ) );

                // print out each chunk
                $usedWidth = 0;

                foreach ( $content as $k => $chunk )
                {

                    $this->restoreFont( $font[ $k ] );

                    $stringWidth = $this->GetStringWidth( $chunk ) + ( $this->ws * substr_count( $chunk, ' ' ) / $this->k );

                    // determine which borders should be used
                    $b = '';

                    if ( $lineCount == 1 && is_int( strpos( $border, 'T' ) ) )
                        $b .= 'T';

                    if ( $k == 0 && is_int( strpos( $border, 'L' ) ) )
                        $b .= 'L';

                    if ( $k == count( $content ) - 1 && is_int( strpos( $border, 'R' ) ) )
                        $b .= 'R';

                    // if it's the last chunk of this line, move to the next line after
                    if ( $k == count( $content ) - 1 )
                        $this->Cell( ( $maxWidth / $this->k ) - $usedWidth + 2 * $this->cMargin, $lineHeight, $chunk, $b, 1, $align, $fill );
                    else
                    {

                        $this->Cell( $stringWidth + 2 * $this->cMargin, $lineHeight, $chunk, $b, 0, $align, $fill );
                        $this->x -= 2 * $this->cMargin;

                    }

                    $usedWidth += $stringWidth;

                }

                // move on to the next line, reset variables, tack on saved content and current char
                $this->restoreFont( $savedFont );

                $font = array( $savedFont );
                $content = array( $savedContent . $s[ $i ] );

                $currContent =& $content[ 0 ];

                $contentWidth = $this->GetStringWidth( $currContent ) * $this->k;
                $cutoffWidth = $contentWidth;

            }

            // another character will fit, so add it on
            else
            {

                $contentWidth += $cw;
                $currContent .= $s[ $i ];

            }

        }

    }

}
?>

Example

This example outputs the same text with the four possible alignments (justified, left-aligned, right-aligned and centered).
<?php
require('flowing_block.php');

$pdf = new PDF_FlowingBlock();

$pdf->AddPage();
$pdf->newFlowingBlock( 40, 6, 'TBLR', 'J' );
$pdf->SetFont( 'Arial', 'B', 16 );
$pdf->WriteFlowingBlock( 'Hello ' );
$pdf->SetFont( 'Arial', 'I', 8 );
$pdf->WriteFlowingBlock( 'World! ' );
$pdf->SetFont( 'Times', '', 10 );
$pdf->WriteFlowingBlock( 'This is a test of the flowing block script.' );
$pdf->SetFont( 'Arial', 'B', 12 );
$pdf->WriteFlowingBlock( ' All' );
$pdf->SetFont( 'Times', '', 10 );
$pdf->WriteFlowingBlock( ' of this should be justified correctly.' . str_repeat( ' This is a test of the flowing block script.', 3 ) );
$pdf->finishFlowingBlock();

$pdf->AddPage();
$pdf->newFlowingBlock( 40, 6, 'TBLR', 'L' );
$pdf->SetFont( 'Arial', 'B', 16 );
$pdf->WriteFlowingBlock( 'Hello ' );
$pdf->SetFont( 'Arial', 'I', 8 );
$pdf->WriteFlowingBlock( 'World! ' );
$pdf->SetFont( 'Times', '', 10 );
$pdf->WriteFlowingBlock( 'This is a test of the flowing block script.' );
$pdf->SetFont( 'Arial', 'B', 12 );
$pdf->WriteFlowingBlock( ' All' );
$pdf->SetFont( 'Times', '', 10 );
$pdf->WriteFlowingBlock( ' of this should be justified correctly.' . str_repeat( ' This is a test of the flowing block script.', 3 ) );
$pdf->finishFlowingBlock();

$pdf->AddPage();
$pdf->newFlowingBlock( 40, 6, 'TBLR', 'R' );
$pdf->SetFont( 'Arial', 'B', 16 );
$pdf->WriteFlowingBlock( 'Hello ' );
$pdf->SetFont( 'Arial', 'I', 8 );
$pdf->WriteFlowingBlock( 'World! ' );
$pdf->SetFont( 'Times', '', 10 );
$pdf->WriteFlowingBlock( 'This is a test of the flowing block script.' );
$pdf->SetFont( 'Arial', 'B', 12 );
$pdf->WriteFlowingBlock( ' All' );
$pdf->SetFont( 'Times', '', 10 );
$pdf->WriteFlowingBlock( ' of this should be justified correctly.' . str_repeat( ' This is a test of the flowing block script.', 3 ) );
$pdf->finishFlowingBlock();

$pdf->AddPage();
$pdf->newFlowingBlock( 40, 6, 'TBLR', 'C' );
$pdf->SetFont( 'Arial', 'B', 16 );
$pdf->WriteFlowingBlock( 'Hello ' );
$pdf->SetFont( 'Arial', 'I', 8 );
$pdf->WriteFlowingBlock( 'World! ' );
$pdf->SetFont( 'Times', '', 10 );
$pdf->WriteFlowingBlock( 'This is a test of the flowing block script.' );
$pdf->SetFont( 'Arial', 'B', 12 );
$pdf->WriteFlowingBlock( ' All' );
$pdf->SetFont( 'Times', '', 10 );
$pdf->WriteFlowingBlock( ' of this should be justified correctly.' . str_repeat( ' This is a test of the flowing block script.', 3 ) );
$pdf->finishFlowingBlock();

$pdf->Output();
?>
View the result here.

Download

ZIP | TGZ