画像処理

【PHP】ImageTTFText()で文字列を縦書き描画する方法

更新日:2023/09/11

PHPのImageTTFText()で文字列を縦書き描画したいと思ったので、関数を作成しました。
最終的には範囲を指定して、その中に縦書き描画するような関数を作成したので紹介します。

 

考え方

ImageTTFText()は文字列を右方向に描画します。
そのため文字列を縦書きするなら、一文字ずつ縦方向に描画していきます。

しかし文字は幅が一定でないため、左端基準で描画すると見た目が悪くなります。

左端基準で文字列を描画

そこで描画する文字の幅を個々に計測して、最大幅を求めます。
そして最大幅の中心に文字を描画していきます。

中心基準で文字列を描画

ImageTTFText()は描画位置をベースラインの左側で指定します。
そのため、左上座標をベースラインの左側に変換する必要があります。
この点については、次のページで紹介しているので参考にしてください。

また括弧等をそのまま描画するのも見た目が悪いです。

括弧等をそのまま描画

そこで、回転して描画します。

括弧等を回転して描画

いろいろ、めんどくさいですが頑張ってコードを作成してみます。

 

文字列を縦書き描画するPHPコード

考え方を元にして作成したPHPコードです。

/*
* 左上指定で文字列を縦書き描画する関数
* $image:  GdImage オブジェクト
* $texts: 描画する文字列
* $fontSize: フォントサイズ
* $fontFile: フォントファイルのパス
* $left , $top : 描画基準位置(左上)
* $color: カラーインデックス
* $characterSpacing: 文字間隔
*/ 
function imageVerticalText( $image , $texts , $fontSize , $fontFile 
        , $left , $top ,  $color , $characterSpacing = 0){
    
            // 回転させる文字の定義
            // angle:回転角度 align: 'c' 中央寄せ 'r' 右 'l' 左
            // c: 対象文字の配列
    $rotateCharactor = [
        ['angle'=>270,'align'=>'c','c'=>['~','-','ー','[',']','(',')','【','】']],
        ['angle'=>270,'align'=>'l','c'=>['」','』']],
        ['angle'=>270,'align'=>'r','c'=>['「','『']],
        ['angle'=>0,'align'=>'r','c'=>['。','、']],
    ];

    $len = mb_strlen($texts);

    $width = 0;
    $currentY = $top;
    $outputTexts = array();  // 文字情報格納用配列

        // 文字列の最大幅を算出
    for( $count = 0;  $count < $len ; $count++ ){
        $c = mb_substr( $texts , $count , 1 );
        
        $angle = 0; $align = 'c';
        foreach( $rotateCharactor as $e ){ // 回転角度等を取得
            if( in_array($c,$e['c']) ) {
                ['angle'=>$angle,'align'=>$align] = $e;break;
            }
        }
        [ $w , $h ,$basex,$basey ] = getWidthAndHeight($fontSize  ,$fontFile,$c,$angle);
        array_push( $outputTexts , compact('c','w','currentY','basex','basey','angle','align') );
        $width = max( $width , $w );
        $currentY += $h + $characterSpacing;
    }
        // 文字列の描画
    foreach( $outputTexts as $v ){
        $py = $v['currentY'] + (-$v['basey']); // ベースライン縦方向算出
        switch($v['align']){ //  // ベースライン左端算出
            case 'r':$px = $left + $width - $v['w'] + (-$v['basex']);break; // 右寄せ
            case 'l':$px = $left + (-$v['basex']);break; // 左寄せ
            default: // 中央
                    $px = $left +  ($width - $v['w'])/2 + (-$v['basex']);
        }
        ImageTTFText($image, $fontSize, $v['angle'], $px, $py, $color, $fontFile, $v['c']);
    }
}

// 文字列の高さ・幅・左上座標を取得(回転対応)
function getWidthAndHeight($fontSize,$fontFile,$texts,$angle){
    $bBox = imagettfbbox($fontSize ,$angle ,$fontFile,$texts);
    $left = min($bBox[0],$bBox[2],$bBox[4],$bBox[6]);
    $right = max($bBox[0],$bBox[2],$bBox[4],$bBox[6]);
    $top = min($bBox[1],$bBox[3],$bBox[5],$bBox[7]);
    $bottom = max($bBox[1],$bBox[3],$bBox[5],$bBox[7]);
    return [$right - $left,$bottom - $top,$left,$top];
}

回転させる文字を$rotateCharactor配列で定義しているのですが、定義しているもの以外にもあると思います。
というか、あります。
描画してみて気になるものがあったら、追加してみてください。
(めんどくさくなったのでないですよ…)

この関数を使って文字列を縦書き描画するコードです。

$texts='文字列を「縦書き」で描画します。';

$left = 30;
$top = 10;

$fontSize = 20;
$fontFile = './ZenMaruGothic-Bold.ttf';

    // 画像の作成
$image = imagecreatetruecolor(100, 450);
$white = imagecolorallocate($image, 255, 255, 255);
imagefill($image, 0, 0, $white);

    // 基準点描画
$red = imagecolorallocate($image, 255, 0, 0);
imageLine($image,$left-30,$top,$left+30,$top,$red);
imageLine($image,$left,$top-30,$left,$top+30,$red);

    // 縦書き描画
$black = imagecolorallocate($image, 0, 0, 0);
imageVerticalText($image,$texts,$fontSize,$fontFile
        ,$left,$top,$black,5);

header("Content-type: image/png");
imagepng($image);
imagedestroy($image);

左上基準がわかるように、赤の十字を描画しています。
適当な名前で保存してブラウザで呼び出すと、次のような画像が描画されます。

描画結果

 

範囲内に縦書き描画する

これで完成だ!
と思ったのですが、範囲内に描画した方が使いやすよね、と思ったので範囲指定版を作成しました。

考え方は一行に描画できる文字を抽出して、範囲内の右側に描画。

さらに一行分抽出して、前回描画した文字の左側に描画します。
これを文字列が終わるまで、または左端からはみ出るまで繰り返します。

/*
* 文字列を範囲内に縦書き描画する関数
* $image:  GdImage オブジェクト
* $texts: 描画する文字列
* $fontSize: フォントサイズ
* $fontFile: フォントファイルのパス
* $left , $top , $right , $bottom: 描画範囲
*       $right=nullのとき左上指定モード、このとき$bottomは高さとみなす
* $color: カラーインデックス
* $characterSpacing: 文字間隔
* $overflowDraw: はみ出し分を描画するかどうか
* $maskMode: $overflowDraw有効時範囲外をマスキングするかどうか
*/
function imageVerticalTextArea( $image , $texts , $fontSize , $fontFile 
        , $left , $top , $right , $bottom ,$color
        ,$characterSpacing = 0,$lineSpacing=0,$overflowDraw=false,$maskMode=false){
    
        // 回転させる文字の定義
        // angle:回転角度 align: 'c' 中央寄せ 'r' 右 'l' 左
        // c: 対象文字の配列
    $rotateCharactor = [
        ['angle'=>270,'align'=>'c','c'=>['~','-','ー','[',']','(',')','【','】']],
        ['angle'=>270,'align'=>'l','c'=>['」','』']],
        ['angle'=>270,'align'=>'r','c'=>['「','『']],
        ['angle'=>0,'align'=>'r','c'=>['。','、']],
    ];

    $areaMode = is_int($right); // 範囲描画モードかどうか
    $notAreaMode = !$areaMode; // 非範囲モードかどうか
    $useBottom = is_int($bottom); // $bottomが指定されているかどうか
    $notOverFlowDraw = !$overflowDraw;

    $defaultWidth = getWidthAndHeight($fontSize  ,$fontFile,'A',0)[0]; // 最小幅
    $len = mb_strlen($texts);
    $count = 0;
    $originalImage = null;

    while( $count < $len ){

        $outputTexts = array();
        $currentY = $top;
        $width = $defaultWidth;

            // 1行の文字列抽出&文字列の最大幅を算出
        for(;  $count < $len ; $count++ ){ 
            $c = mb_substr( $texts , $count , 1 );
            if( $c === "\n" ) {$count++;break;} // 改行→行終了
                
            $angle = 0; $align = 'c';
            foreach( $rotateCharactor as $e ){ // 回転角度等を取得
                if( in_array($c,$e['c']) ) {
                    ['angle'=>$angle,'align'=>$align] = $e;break;
                }
            }
            [ $w , $h ,$basex,$basey ] = getWidthAndHeight($fontSize  ,$fontFile,$c,$angle);
            if( $areaMode && $notOverFlowDraw && $left > $right - $w ) return; // 左端を超えた → 終了

            $bottomOverflow = $useBottom && $bottom < $currentY + $h;
            if( $areaMode && $bottomOverflow ) break; // 範囲モードで下端を超えた
            if( $notAreaMode && $bottomOverflow && $notOverFlowDraw ) break; // 非範囲モードで下端を超えた
        
            array_push( $outputTexts , compact('c','w','currentY','basex','basey','angle','align') );
            $width = max( $width , $w );
            $currentY += $h + $characterSpacing;
            if( $useBottom && $currentY > $bottom ) {$count++;break;} // 次の文字が下端を超える
        }
    
        $cx = $notAreaMode ? $left + $width : $right; // 右座標取得

            // $maskModeオン(範囲外を描画しない)& 範囲外に描画されるときマスク処理
        if( $maskMode  && $overflowDraw
            && ( ($notAreaMode && $useBottom ) // 非範囲モード:下方にオーバーフロー
            || ( $areaMode && $cx - $width < $left ) // 範囲いモード:左にオーバーフロー 
            )){
            $originalImage = $image;
            $maskWidth = $cx-$left+1; $maskHeight = $bottom-$top+1;
                // 1行分だけ新規画像にコピー
            $image = imagecreatetruecolor($maskWidth, $maskHeight);
            imagecopy( $image , $originalImage , 0 , 0 , $left , $top , $maskWidth , $maskHeight );
            foreach( $outputTexts as &$v1 ){ $v1['currentY'] -= $top; } // 座標調整
            $cx -= $left;
        }
            // 文字列の描画
        foreach( $outputTexts as $v ){
            $py = $v['currentY'] + (-$v['basey']); // ベースライン(y)算出
            switch($v['align']){ //  // ベースライン左端(x)算出
                case 'r':$px = $cx - $v['w'] + (-$v['basex']);break; // 右寄せ
                case 'l':$px = $cx - $width + (-$v['basex']);break; // 左寄せ
                default: // 中央
                    $px = $cx - ($width/2) - ($v['w']/2) + (-$v['basex']);
            }
            ImageTTFText($image, $fontSize, $v['angle'], $px, $py, $color, $fontFile, $v['c']);
        }
            // マスク中なら元画像に貼り付け
        if( $originalImage !== null ){ 
            imagecopy( $originalImage , $image  , $left , $top ,  0 , 0 , $maskWidth , $maskHeight );
            imagedestroy($image);return;
        }
        if( $notAreaMode ) return; // 非範囲モードなら終了
        $right -= $width + $lineSpacing; // 右座標を次の行に進める
        if( $right < $left ) return; // 範囲外なら終了
    }
}
// 文字列の高さ・幅・左上座標を取得
function getWidthAndHeight($fontSize,$fontFile,$texts,$angle){
    $bBox = imagettfbbox($fontSize ,$angle ,$fontFile,$texts);
    $left = min($bBox[0],$bBox[2],$bBox[4],$bBox[6]);
    $right = max($bBox[0],$bBox[2],$bBox[4],$bBox[6]);
    $top = min($bBox[1],$bBox[3],$bBox[5],$bBox[7]);
    $bottom = max($bBox[1],$bBox[3],$bBox[5],$bBox[7]);
    return [$right - $left,$bottom - $top,$left,$top];
}
/*
* 左上指定で文字列を縦書き描画する関数
* $image:  GdImage オブジェクト
* $texts: 描画する文字列
* $fontSize: フォントサイズ
* $fontFile: フォントファイルのパス
* $left , $top : 描画基準位置(左上)
* $color: カラーインデックス
* $characterSpacing: 文字間隔
*/ 
function imageVerticalText( $image , $texts , $fontSize , $fontFile 
        , $left , $top , $color , $characterSpacing = 0){

    imageVerticalTextArea($image , $texts , $fontSize , $fontFile 
        , $left , $top  , null,null, $color,$characterSpacing);
}
/*
* 左上指定で文字列を縦書き描画する関数(高さ指定あり)
* $image:  GdImage オブジェクト
* $texts: 描画する文字列
* $fontSize: フォントサイズ
* $fontFile: フォントファイルのパス
* $left , $top : 描画基準位置(左上)
* $height : 高さ
* $color: カラーインデックス
* $characterSpacing: 文字間隔
* $overflowDraw: はみ出し分を描画するかどうか
* $maskMode: $overflowDraw有効時範囲外をマスキングするかどうか
*/ 
function imageVerticalText2( $image , $texts , $fontSize , $fontFile 
        , $left , $top , $height , $color , $characterSpacing = 0,$overflowDraw=false,$maskMode=false){
    
    $height = (is_int($height) &&  $height > 0 ) ? $top + $height : null;

    imageVerticalTextArea($image , $texts , $fontSize , $fontFile 
        , $left , $top  , null,$height, $color,$characterSpacing,0,$overflowDraw,$maskMode);
}

引数が多いですね…

最後の方の$overflowDrawは、文字の一部が範囲外に溢れたとき描画するかどうかを指定します。
最後の$maskModeは、溢れたときに範囲外を描画しないようにマスキングするかどうかを指定します。

まずは両方ともオフの時の例です。

$texts=<<<EOF
文字列を縦書きで描画します。

「abcd」などの半角文字と、「あいうえお」などの全角文字は文字幅が異なるので工夫が必要です。


改行のみの行にも対応しています。
この行は範囲外なので表示されません。
EOF;

    // 改行を \nに統一
$texts = str_replace( ["\r\n","\r"], "\n" , $texts );

$left = 50;
$top = 50;
$right = 450;
$bottom = 300;

$fontSize = 20;
$fontFile = './ZenMaruGothic-Bold.ttf';

$image = imagecreatetruecolor(500, 350);
$white = imagecolorallocate($image, 255, 255, 255);
imagefill($image, 0, 0, $white);
  // 範囲がわかりやすように枠を病害
$red = imagecolorallocate($image, 255, 0, 0);
imagerectangle($image,$left,$top,$right,$bottom,$red);

$black = imagecolorallocate($image, 0, 0, 0);

  // 文字列を縦書き描画
imageVerticalTextArea($image,$texts,$fontSize,$fontFile
    ,$left,$top,$right,$bottom,$black,5,8);

header("Content-type: image/png");
imagepng($image);
imagedestroy($image);

改行は "\n" のみ対応しているので、改行を統一しています。

適当な名前で保存してブラウザで呼び出すと、次のような画像が描画されます。

範囲縦書き描画の結果

次は、imageVerticalTextArea()の$maskModeをtrue指定に変更します。

imageVerticalTextArea($image,$texts,$fontSize,$fontFile
    ,$left,$top,$right,$bottom,$black,5,8,true);

次のように、一部分がはみ出した文字列が描画されます。
完全にはみ出している文字列は描画されません。

一部分がはみ出した文字列が描画される

次は、imageVerticalTextArea()の$overflowDrawをtrue指定に変更します。

imageVerticalTextArea($image,$texts,$fontSize,$fontFile
    ,$left,$top,$right,$bottom,$black,5,8,true,true);

すると、はみ出した文字の範囲内のみ描画されます。

一部分がはみ出した文字列の範囲内のみ描画される

コード内に前項で紹介したimageVerticalText()と同名の関数がありますが、この関数は内部でimageVerticalTextArea()を呼び出しています。

imageVerticalTextArea()は、引数$rightにnullを指定することで、左上を基準とした縦書き描画に対応しています。
また$rightがnullのとき$bottomが数値なら、$bottomは高さとみなされます。

最後の関数imageVerticalText2()は、左上基準かつ高さ指定ありでimageVerticalTextArea()を呼び出しています。

更新日:2023/09/11

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。